mirror of
https://github.com/ProtonMail/android-mail.git
synced 2026-05-15 09:50:40 +00:00
Add actions to copy and download raw data
ET-5543 ET-5545
This commit is contained in:
committed by
Niccolò Forlini
parent
1322343b9c
commit
cd8d33f611
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.mailcommon.data.file
|
||||
|
||||
import java.io.IOException
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExternalFileStorage @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun saveDataToDownloads(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
data: String
|
||||
): Either<DataError, Unit> = either {
|
||||
withContext(Dispatchers.IO) {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
|
||||
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
?: raise(DataError.Local.FailedToStoreFile)
|
||||
try {
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(data.toByteArray())
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
raise(DataError.Local.FailedToStoreFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -19,7 +19,9 @@
|
||||
package ch.protonmail.android.maildetail.dagger
|
||||
|
||||
import ch.protonmail.android.maildetail.data.repository.InMemoryConversationStateRepositoryImpl
|
||||
import ch.protonmail.android.maildetail.data.repository.RawMessageDataRepositoryImpl
|
||||
import ch.protonmail.android.maildetail.domain.repository.InMemoryConversationStateRepository
|
||||
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -34,4 +36,7 @@ internal interface ViewModelBindings {
|
||||
implementation: InMemoryConversationStateRepositoryImpl
|
||||
): InMemoryConversationStateRepository
|
||||
|
||||
@Binds
|
||||
fun bindRawMessageDataRepository(implementation: RawMessageDataRepositoryImpl): RawMessageDataRepository
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.module.data)
|
||||
implementation(project(":mail-common:data"))
|
||||
implementation(project(":mail-common:domain"))
|
||||
implementation(project(":mail-detail:domain"))
|
||||
implementation(project(":mail-message:domain"))
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.maildetail.data.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.data.file.ExternalFileStorage
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class RawMessageDataRepositoryImpl @Inject constructor(
|
||||
private val externalFileStorage: ExternalFileStorage
|
||||
) : RawMessageDataRepository {
|
||||
|
||||
override suspend fun downloadRawData(fileName: String, data: String): Either<DataError, Unit> =
|
||||
externalFileStorage.saveDataToDownloads(fileName, "text/plain", data)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.maildetail.data.repository
|
||||
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.mailcommon.data.file.ExternalFileStorage
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class RawMessageDataRepositoryImplTest {
|
||||
|
||||
private val externalFileStorage = mockk<ExternalFileStorage>()
|
||||
|
||||
private val rawMessageDataRepository = RawMessageDataRepositoryImpl(externalFileStorage)
|
||||
|
||||
@Test
|
||||
fun `should call external file storage method when download raw data is called`() = runTest {
|
||||
// Given
|
||||
val fileName = "headers"
|
||||
val data = "raw headers"
|
||||
coEvery { externalFileStorage.saveDataToDownloads(fileName, "text/plain", data) } returns Unit.right()
|
||||
|
||||
// When
|
||||
val actual = rawMessageDataRepository.downloadRawData(fileName, data)
|
||||
|
||||
// Then
|
||||
coVerify { externalFileStorage.saveDataToDownloads(fileName, "text/plain", data) }
|
||||
assertEquals(Unit.right(), actual)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.maildetail.domain.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
|
||||
interface RawMessageDataRepository {
|
||||
|
||||
suspend fun downloadRawData(fileName: String, data: String): Either<DataError, Unit>
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.maildetail.domain.usecase
|
||||
|
||||
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DownloadRawMessageData @Inject constructor(
|
||||
private val rawMessageDataRepository: RawMessageDataRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(fileName: String, data: String) =
|
||||
rawMessageDataRepository.downloadRawData(fileName, data)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and Proton Mail.
|
||||
*
|
||||
* Proton Mail is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Mail is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.maildetail.domain.usecase
|
||||
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DownloadRawMessageDataTest {
|
||||
|
||||
private val rawMessageDataRepository = mockk<RawMessageDataRepository>()
|
||||
|
||||
private val downloadRawMessageData = DownloadRawMessageData(rawMessageDataRepository)
|
||||
|
||||
@Test
|
||||
fun `should call repository method when use case is called`() = runTest {
|
||||
// Given
|
||||
val fileName = "headers"
|
||||
val data = "raw headers"
|
||||
coEvery { rawMessageDataRepository.downloadRawData(fileName, data) } returns Unit.right()
|
||||
|
||||
// When
|
||||
val actual = downloadRawMessageData(fileName, data)
|
||||
|
||||
// Then
|
||||
coVerify { rawMessageDataRepository.downloadRawData(fileName, data) }
|
||||
assertEquals(Unit.right(), actual)
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -18,12 +18,18 @@
|
||||
|
||||
package ch.protonmail.android.maildetail.presentation.model
|
||||
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
|
||||
sealed class RawMessageDataState {
|
||||
abstract val type: RawMessageDataType
|
||||
|
||||
data class Loading(override val type: RawMessageDataType) : RawMessageDataState()
|
||||
data class Error(override val type: RawMessageDataType) : RawMessageDataState()
|
||||
data class Data(override val type: RawMessageDataType, val data: String) : RawMessageDataState()
|
||||
data class Data(
|
||||
override val type: RawMessageDataType,
|
||||
val data: String,
|
||||
val toast: Effect<Int>
|
||||
) : RawMessageDataState()
|
||||
}
|
||||
|
||||
enum class RawMessageDataType { Headers, HTML }
|
||||
|
||||
+106
-62
@@ -18,6 +18,9 @@
|
||||
|
||||
package ch.protonmail.android.maildetail.presentation.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -36,12 +39,15 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import ch.protonmail.android.design.compose.component.ProtonCenteredProgress
|
||||
import ch.protonmail.android.design.compose.component.ProtonErrorMessage
|
||||
import ch.protonmail.android.design.compose.component.appbar.ProtonTopAppBar
|
||||
@@ -49,7 +55,10 @@ import ch.protonmail.android.design.compose.theme.ProtonDimens
|
||||
import ch.protonmail.android.design.compose.theme.ProtonTheme
|
||||
import ch.protonmail.android.design.compose.theme.bodyLargeNorm
|
||||
import ch.protonmail.android.design.compose.theme.bodyMediumNorm
|
||||
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
|
||||
import ch.protonmail.android.mailcommon.presentation.extension.copyTextToClipboard
|
||||
import ch.protonmail.android.maildetail.presentation.R
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
|
||||
@@ -57,13 +66,27 @@ import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataVie
|
||||
|
||||
@Composable
|
||||
fun RawMessageDataScreen(onBackClick: () -> Unit, viewModel: RawMessageDataViewModel = hiltViewModel()) {
|
||||
RawMessageDataScreen(viewModel.state.collectAsStateWithLifecycle().value, onBackClick)
|
||||
RawMessageDataScreen(
|
||||
viewModel.state.collectAsStateWithLifecycle().value,
|
||||
onBackClick
|
||||
) { type, data -> viewModel.downloadData(type, data) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
|
||||
fun RawMessageDataScreen(
|
||||
state: RawMessageDataState,
|
||||
onBackClick: () -> Unit,
|
||||
onDownloadData: (RawMessageDataType, String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isDropDownExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
if (state is RawMessageDataState.Data) {
|
||||
ConsumableLaunchedEffect(state.toast) {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = ProtonTheme.colors.backgroundInvertedNorm,
|
||||
topBar = {
|
||||
@@ -92,68 +115,81 @@ fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { isDropDownExpanded.value = true }
|
||||
) {
|
||||
Icon(
|
||||
tint = ProtonTheme.colors.iconNorm,
|
||||
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.raw_message_data_more_button_content_description
|
||||
if (state is RawMessageDataState.Data) {
|
||||
IconButton(
|
||||
onClick = { isDropDownExpanded.value = true }
|
||||
) {
|
||||
Icon(
|
||||
tint = ProtonTheme.colors.iconNorm,
|
||||
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.raw_message_data_more_button_content_description
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = { isDropDownExpanded.value = false },
|
||||
containerColor = ProtonTheme.colors.backgroundInvertedNorm
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_download_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_arrow_down_line),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
DropdownMenu(
|
||||
expanded = isDropDownExpanded.value,
|
||||
onDismissRequest = { isDropDownExpanded.value = false },
|
||||
containerColor = ProtonTheme.colors.backgroundInvertedNorm
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_download_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
onDownloadData(state.type, state.data)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_arrow_down_line),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_copy_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
context.copyTextToClipboard(state.type.name, state.data)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_squares),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
)
|
||||
if (state.type == RawMessageDataType.Headers) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_learn_more_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
isDropDownExpanded.value = false
|
||||
openLearnMoreLink(context)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_info_circle),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_copy_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_squares),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.raw_message_data_learn_more_action),
|
||||
style = ProtonTheme.typography.bodyLargeNorm
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_proton_info_circle),
|
||||
contentDescription = NO_CONTENT_DESCRIPTION
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -188,6 +224,12 @@ fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLearnMoreLink(context: Context) {
|
||||
val uri = "https://proton.me/blog/what-are-email-headers".toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RawMessageDataScreenPreview() {
|
||||
@@ -238,9 +280,11 @@ fun RawMessageDataScreenPreview() {
|
||||
User-Agent: ProtonMail/Android-5.5.3
|
||||
X-Proton-Sender-Ip: 83.212.41.102
|
||||
X-Proton-Relay: yes
|
||||
""".trimIndent()
|
||||
""".trimIndent(),
|
||||
toast = Effect.empty()
|
||||
),
|
||||
onBackClick = {}
|
||||
onBackClick = {},
|
||||
onDownloadData = { _, _ -> }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -21,6 +21,9 @@ package ch.protonmail.android.maildetail.presentation.viewmodel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
import ch.protonmail.android.maildetail.domain.usecase.DownloadRawMessageData
|
||||
import ch.protonmail.android.maildetail.presentation.R
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
|
||||
import ch.protonmail.android.maildetail.presentation.ui.RawMessageDataScreen
|
||||
@@ -34,12 +37,14 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.util.kotlin.deserializeOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RawMessageDataViewModel @Inject constructor(
|
||||
private val downloadRawMessageData: DownloadRawMessageData,
|
||||
private val getRawMessageBody: GetRawMessageBody,
|
||||
private val getRawMessageHeaders: GetRawMessageHeaders,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
@@ -59,11 +64,11 @@ class RawMessageDataViewModel @Inject constructor(
|
||||
val event = when (rawMessageDataType) {
|
||||
RawMessageDataType.Headers -> getRawMessageHeaders(primaryUserId.first(), messageId).fold(
|
||||
ifLeft = { RawMessageDataState.Error(rawMessageDataType) },
|
||||
ifRight = { RawMessageDataState.Data(rawMessageDataType, it) }
|
||||
ifRight = { RawMessageDataState.Data(rawMessageDataType, it, Effect.empty()) }
|
||||
)
|
||||
RawMessageDataType.HTML -> getRawMessageBody(primaryUserId.first(), messageId).fold(
|
||||
ifLeft = { RawMessageDataState.Error(rawMessageDataType) },
|
||||
ifRight = { RawMessageDataState.Data(rawMessageDataType, it) }
|
||||
ifRight = { RawMessageDataState.Data(rawMessageDataType, it, Effect.empty()) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,6 +76,33 @@ class RawMessageDataViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadData(type: RawMessageDataType, data: String) = viewModelScope.launch {
|
||||
val fileName = when (type) {
|
||||
RawMessageDataType.Headers -> "headers"
|
||||
RawMessageDataType.HTML -> "html"
|
||||
}
|
||||
downloadRawMessageData(fileName, data).fold(
|
||||
ifLeft = {
|
||||
mutableState.update {
|
||||
if (it is RawMessageDataState.Data) {
|
||||
it.copy(toast = Effect.of(R.string.raw_message_data_failed_download))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
mutableState.update {
|
||||
if (it is RawMessageDataState.Data) {
|
||||
it.copy(toast = Effect.of(R.string.raw_message_data_successful_download))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun requireMessageId(): MessageId {
|
||||
val messageIdParam = savedStateHandle.get<String>(RawMessageDataScreen.MESSAGE_ID_KEY)
|
||||
?: throw IllegalStateException("No Message id given")
|
||||
|
||||
@@ -191,4 +191,6 @@
|
||||
<string name="raw_message_data_copy_action">Copy</string>
|
||||
<string name="raw_message_data_learn_more_action">Learn more</string>
|
||||
<string name="raw_message_data_error">Error loading data</string>
|
||||
<string name="raw_message_data_successful_download">Download completed</string>
|
||||
<string name="raw_message_data_failed_download">Download failed</string>
|
||||
</resources>
|
||||
|
||||
+55
-2
@@ -23,8 +23,11 @@ import app.cash.turbine.test
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import ch.protonmail.android.mailcommon.domain.model.DataError
|
||||
import ch.protonmail.android.mailcommon.presentation.Effect
|
||||
import ch.protonmail.android.maildetail.domain.usecase.DownloadRawMessageData
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
|
||||
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
|
||||
import ch.protonmail.android.maildetail.presentation.R
|
||||
import ch.protonmail.android.maildetail.presentation.ui.RawMessageDataScreen
|
||||
import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataViewModelTest.TestData.MESSAGE_ID
|
||||
import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataViewModelTest.TestData.RAW_DATA_TYPE_HEADERS
|
||||
@@ -49,6 +52,7 @@ import kotlin.test.assertEquals
|
||||
|
||||
class RawMessageDataViewModelTest {
|
||||
|
||||
private val downloadRawMessageData = mockk<DownloadRawMessageData>()
|
||||
private val getRawMessageBody = mockk<GetRawMessageBody>()
|
||||
private val getRawMessageHeaders = mockk<GetRawMessageHeaders>()
|
||||
private val savedStateHandle = mockk<SavedStateHandle> {
|
||||
@@ -60,6 +64,7 @@ class RawMessageDataViewModelTest {
|
||||
|
||||
private val rawMessageDataViewModel by lazy {
|
||||
RawMessageDataViewModel(
|
||||
downloadRawMessageData = downloadRawMessageData,
|
||||
getRawMessageBody = getRawMessageBody,
|
||||
getRawMessageHeaders = getRawMessageHeaders,
|
||||
savedStateHandle = savedStateHandle,
|
||||
@@ -86,7 +91,8 @@ class RawMessageDataViewModelTest {
|
||||
// Then
|
||||
val expected = RawMessageDataState.Data(
|
||||
type = RawMessageDataType.Headers,
|
||||
data = rawHeaders
|
||||
data = rawHeaders,
|
||||
toast = Effect.empty()
|
||||
)
|
||||
assertEquals(expected, awaitItem())
|
||||
}
|
||||
@@ -122,7 +128,8 @@ class RawMessageDataViewModelTest {
|
||||
// Then
|
||||
val expected = RawMessageDataState.Data(
|
||||
type = RawMessageDataType.HTML,
|
||||
data = rawBody
|
||||
data = rawBody,
|
||||
toast = Effect.empty()
|
||||
)
|
||||
assertEquals(expected, awaitItem())
|
||||
}
|
||||
@@ -144,6 +151,52 @@ class RawMessageDataViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit state with success toast when downloading data is successful`() = runTest {
|
||||
// Given
|
||||
val messageId = MessageId(MESSAGE_ID)
|
||||
val rawBody = "raw body"
|
||||
every { savedStateHandle.get<String>(RawMessageDataScreen.RAW_DATA_TYPE_KEY) } returns RAW_DATA_TYPE_HTML
|
||||
coEvery { getRawMessageBody(UserIdTestData.userId, messageId) } returns rawBody.right()
|
||||
coEvery { downloadRawMessageData("html", rawBody) } returns Unit.right()
|
||||
|
||||
rawMessageDataViewModel.state.test {
|
||||
awaitItem()
|
||||
|
||||
// When
|
||||
rawMessageDataViewModel.downloadData(RawMessageDataType.HTML, rawBody)
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
R.string.raw_message_data_successful_download,
|
||||
(awaitItem() as RawMessageDataState.Data).toast.consume()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit state with failure toast when downloading data failed`() = runTest {
|
||||
// Given
|
||||
val messageId = MessageId(MESSAGE_ID)
|
||||
val rawHeaders = "raw headers"
|
||||
every { savedStateHandle.get<String>(RawMessageDataScreen.RAW_DATA_TYPE_KEY) } returns RAW_DATA_TYPE_HEADERS
|
||||
coEvery { getRawMessageHeaders(UserIdTestData.userId, messageId) } returns rawHeaders.right()
|
||||
coEvery { downloadRawMessageData("headers", rawHeaders) } returns DataError.Local.Unknown.left()
|
||||
|
||||
rawMessageDataViewModel.state.test {
|
||||
awaitItem()
|
||||
|
||||
// When
|
||||
rawMessageDataViewModel.downloadData(RawMessageDataType.Headers, rawHeaders)
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
R.string.raw_message_data_failed_download,
|
||||
(awaitItem() as RawMessageDataState.Data).toast.consume()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object TestData {
|
||||
|
||||
const val MESSAGE_ID = "message_id"
|
||||
|
||||
Reference in New Issue
Block a user