PM-34544: bug: Handle large attachments in preview (#6757)

This commit is contained in:
David Perez
2026-04-03 14:55:48 +00:00
committed by GitHub
parent ab583296aa
commit 61955d7cbe
22 changed files with 304 additions and 75 deletions
@@ -203,13 +203,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = it),
)
},
onNavigateToPreviewAttachment = { cipherId, attachmentId, fileName ->
navController.navigateToPreviewAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
fileName = fileName,
)
},
onNavigateToPreviewAttachment = { navController.navigateToPreviewAttachment(it) },
)
vaultQrCodeScanDestination(
onNavigateToManualCodeEntryScreen = {
@@ -260,13 +254,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
)
attachmentDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToPreviewAttachment = { cipherId, attachmentId, fileName ->
navController.navigateToPreviewAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
fileName = fileName,
)
},
onNavigateToPreviewAttachment = { navController.navigateToPreviewAttachment(it) },
)
setupUnlockDestination(
onNavigateBack = {
@@ -6,6 +6,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
import kotlinx.serialization.Serializable
/**
@@ -34,11 +35,7 @@ fun SavedStateHandle.toAttachmentsArgs(): AttachmentsArgs {
*/
fun NavGraphBuilder.attachmentDestination(
onNavigateBack: () -> Unit,
onNavigateToPreviewAttachment: (
cipherId: String,
attachmentId: String,
fileName: String,
) -> Unit,
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
) {
composableWithSlideTransitions<AttachmentsRoute> {
AttachmentsScreen(
@@ -32,6 +32,7 @@ import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
/**
* Displays the attachments screen.
@@ -43,7 +44,7 @@ fun AttachmentsScreen(
viewModel: AttachmentsViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
onNavigateBack: () -> Unit,
onNavigateToPreview: (cipherId: String, attachmentId: String, fileName: String) -> Unit,
onNavigateToPreview: (route: PreviewAttachmentRoute) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val attachmentsHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) }
@@ -65,7 +66,15 @@ fun AttachmentsScreen(
is AttachmentsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
is AttachmentsEvent.NavigateToPreview -> {
onNavigateToPreview(event.cipherId, event.attachmentId, event.fileName)
onNavigateToPreview(
PreviewAttachmentRoute(
cipherId = event.cipherId,
attachmentId = event.attachmentId,
fileName = event.fileName,
displaySize = event.displaySize,
isLargeFile = event.isLargeFile,
),
)
}
}
}
@@ -247,6 +247,8 @@ class AttachmentsViewModel @Inject constructor(
cipherId = state.cipherId,
attachmentId = action.attachment.id,
fileName = action.attachment.title,
displaySize = action.attachment.displaySize,
isLargeFile = action.attachment.isLargeFile,
),
)
}
@@ -474,6 +476,7 @@ data class AttachmentsState(
val id: String,
val title: String,
val displaySize: String,
val isLargeFile: Boolean,
) : Parcelable
/**
@@ -527,6 +530,8 @@ sealed class AttachmentsEvent {
val cipherId: String,
val attachmentId: String,
val fileName: String,
val displaySize: String,
val isLargeFile: Boolean,
) : AttachmentsEvent()
/**
@@ -16,6 +16,8 @@ data class PreviewAttachmentRoute(
val cipherId: String,
val attachmentId: String,
val fileName: String,
val displaySize: String,
val isLargeFile: Boolean,
)
/**
@@ -25,6 +27,8 @@ data class PreviewAttachmentArgs(
val cipherId: String,
val attachmentId: String,
val fileName: String,
val displaySize: String,
val isLargeFile: Boolean,
)
/**
@@ -36,6 +40,8 @@ fun SavedStateHandle.toPreviewAttachmentArgs(): PreviewAttachmentArgs {
cipherId = route.cipherId,
attachmentId = route.attachmentId,
fileName = route.fileName,
displaySize = route.displaySize,
isLargeFile = route.isLargeFile,
)
}
@@ -56,17 +62,11 @@ fun NavGraphBuilder.previewAttachmentDestination(
* Navigate to the preview attachment screen.
*/
fun NavController.navigateToPreviewAttachment(
cipherId: String,
attachmentId: String,
fileName: String,
route: PreviewAttachmentRoute,
navOptions: NavOptions? = null,
) {
navigate(
route = PreviewAttachmentRoute(
cipherId = cipherId,
attachmentId = attachmentId,
fileName = fileName,
),
route = route,
navOptions = navOptions,
)
}
@@ -11,6 +11,8 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.annotatedStringResource
import com.bitwarden.ui.platform.base.util.spanStyleOf
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
@@ -19,6 +21,7 @@ import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.preview.ImagePreviewContent
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@@ -29,6 +32,7 @@ import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.asText
/**
@@ -65,6 +69,9 @@ fun PreviewAttachmentScreen(
dialogState = state.dialogState,
onDismissRequest = { viewModel.trySendAction(PreviewAttachmentAction.DismissDialog) },
onCloseClick = { viewModel.trySendAction(PreviewAttachmentAction.CloseClick) },
onConfirmDownloadClick = {
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
},
)
BitwardenScaffold(
@@ -139,6 +146,7 @@ private fun PreviewAttachmentDialogs(
dialogState: PreviewAttachmentState.DialogState?,
onDismissRequest: () -> Unit,
onCloseClick: () -> Unit,
onConfirmDownloadClick: () -> Unit,
) {
when (dialogState) {
is PreviewAttachmentState.DialogState.Error -> {
@@ -165,6 +173,25 @@ private fun PreviewAttachmentDialogs(
)
}
is PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation -> {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.download_attachment),
message = annotatedStringResource(
id = BitwardenString.attachment_large_warning,
args = arrayOf(dialogState.displaySize),
style = spanStyleOf(
color = BitwardenTheme.colorScheme.text.primary,
textStyle = BitwardenTheme.typography.bodyMedium,
),
),
confirmButtonText = stringResource(id = BitwardenString.yes),
dismissButtonText = stringResource(id = BitwardenString.no),
onConfirmClick = onConfirmDownloadClick,
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
}
null -> Unit
}
}
@@ -53,19 +53,27 @@ class PreviewAttachmentViewModel @Inject constructor(
initialState = savedStateHandle[KEY_STATE] ?: run {
val args = savedStateHandle.toPreviewAttachmentArgs()
val isPreviewable = args.fileName.isPreviewable
val isLargeFile = args.isLargeFile
PreviewAttachmentState(
cipherId = args.cipherId,
attachmentId = args.attachmentId,
fileName = args.fileName,
displaySize = args.displaySize,
isLargeFile = isLargeFile,
isPreviewable = isPreviewable,
viewState = if (isPreviewable) {
PreviewAttachmentState.ViewState.Loading()
} else {
viewState = if (!isPreviewable) {
PreviewAttachmentState.ViewState.Error(
message = BitwardenString
.preview_not_available_for_files
.asText(args.fileName.fileExtension),
)
} else if (isLargeFile) {
PreviewAttachmentState.ViewState.Error(
message = BitwardenString.this_file_is_too_large_to_preview.asText(),
illustrationRes = BitwardenDrawable.ill_file_error,
)
} else {
PreviewAttachmentState.ViewState.Loading()
},
dialogState = null,
)
@@ -88,7 +96,7 @@ class PreviewAttachmentViewModel @Inject constructor(
refreshDataFlow
.filter {
// Don't bother retrieving the file is we cannot display it.
state.isPreviewable
state.isPreviewable && !state.isLargeFile
}
.flatMapLatest {
vaultRepository
@@ -106,6 +114,7 @@ class PreviewAttachmentViewModel @Inject constructor(
PreviewAttachmentAction.CloseClick -> handleCloseClick()
PreviewAttachmentAction.DismissDialog -> handleDismissDialog()
PreviewAttachmentAction.DownloadClick -> handleDownloadClick()
PreviewAttachmentAction.ConfirmDownloadClick -> handleConfirmDownloadClick()
PreviewAttachmentAction.BitmapRenderComplete -> handleBitmapRenderComplete()
PreviewAttachmentAction.BitmapRenderError -> handleBitmapRenderError()
PreviewAttachmentAction.FileMissing -> handleFileMissing()
@@ -316,6 +325,24 @@ class PreviewAttachmentViewModel @Inject constructor(
}
private fun handleDownloadClick() {
if (state.isLargeFile) {
mutableStateFlow.update {
it.copy(
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
displaySize = state.displaySize,
),
)
}
return
}
downloadFile()
}
private fun handleConfirmDownloadClick() {
downloadFile()
}
private fun downloadFile() {
mutableStateFlow.update {
it.copy(
dialogState = PreviewAttachmentState.DialogState.Loading(
@@ -390,6 +417,8 @@ data class PreviewAttachmentState(
val cipherId: String,
val attachmentId: String,
val fileName: String,
val displaySize: String,
val isLargeFile: Boolean,
val isPreviewable: Boolean,
val viewState: ViewState,
val dialogState: DialogState?,
@@ -452,6 +481,14 @@ data class PreviewAttachmentState(
*/
@Parcelize
data object PreviewUnavailable : DialogState()
/**
* Represents a warning dialog when the user is about to download a large file.
*/
@Parcelize
data class DownloadLargeFileConfirmation(
val displaySize: String,
) : DialogState()
}
}
@@ -517,6 +554,11 @@ sealed class PreviewAttachmentAction {
*/
data object DownloadClick : PreviewAttachmentAction()
/**
* User clicked to confirm that they want to download the file.
*/
data object ConfirmDownloadClick : PreviewAttachmentAction()
/**
* The bitmap has been rendered from file.
*/
@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.ui.vault.feature.attachments.util
import com.bitwarden.vault.AttachmentView
private const val TEN_MB_IN_BYTES: Long = 10485760L
/**
* @return `true` if the file is larger than 10MB, `false` otherwise.
*/
fun AttachmentView.isLargeFile(): Boolean =
try {
(this.size?.toLong() ?: 0L) >= TEN_MB_IN_BYTES
} catch (_: NumberFormatException) {
false
}
@@ -19,6 +19,7 @@ fun CipherView.toViewState(): AttachmentsState.ViewState.Content =
id = id,
title = it.fileName.orEmpty(),
displaySize = it.sizeName.orEmpty(),
isLargeFile = it.isLargeFile(),
)
}
.toImmutableList(),
@@ -7,6 +7,7 @@ import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import kotlinx.serialization.Serializable
@@ -45,11 +46,7 @@ fun NavGraphBuilder.vaultItemDestination(
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
onNavigateToPreviewAttachment: (
cipherId: String,
attachmentId: String,
fileName: String,
) -> Unit,
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
) {
composableWithSlideTransitions<VaultItemRoute> {
VaultItemScreen(
@@ -42,6 +42,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
@@ -63,11 +64,7 @@ fun VaultItemScreen(
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
onNavigateToPreviewAttachment: (
cipherId: String,
attachmentId: String,
fileName: String,
) -> Unit,
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
@@ -122,7 +119,15 @@ fun VaultItemScreen(
}
is VaultItemEvent.NavigateToPreviewAttachment -> {
onNavigateToPreviewAttachment(event.cipherId, event.attachmentId, event.fileName)
onNavigateToPreviewAttachment(
PreviewAttachmentRoute(
cipherId = event.cipherId,
attachmentId = event.attachmentId,
fileName = event.fileName,
displaySize = event.displaySize,
isLargeFile = event.isLargeFile,
),
)
}
}
}
@@ -423,6 +423,8 @@ class VaultItemViewModel @Inject constructor(
cipherId = state.vaultItemId,
attachmentId = action.attachment.id,
fileName = action.attachment.title,
displaySize = action.attachment.displaySize,
isLargeFile = action.attachment.isLargeFile,
),
)
}
@@ -2008,6 +2010,8 @@ sealed class VaultItemEvent {
val cipherId: String,
val attachmentId: String,
val fileName: String,
val displaySize: String,
val isLargeFile: Boolean,
) : VaultItemEvent()
/**
@@ -20,7 +20,9 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.annotatedStringResource
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.spanStyleOf
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.model.CardStyle
@@ -57,10 +59,6 @@ fun VaultItemAttachment(
shouldShowPremiumWarningDialog = true
return@cardStyle
}
if (attachmentItem.isLargeFile) {
shouldShowSizeWarningDialog = true
return@cardStyle
}
onAttachmentPreviewClick(attachmentItem)
},
)
@@ -138,10 +136,14 @@ fun VaultItemAttachment(
if (shouldShowSizeWarningDialog) {
BitwardenTwoButtonDialog(
title = null,
message = stringResource(
BitwardenString.attachment_large_warning,
attachmentItem.displaySize,
title = stringResource(id = BitwardenString.download_attachment),
message = annotatedStringResource(
id = BitwardenString.attachment_large_warning,
args = arrayOf(attachmentItem.displaySize),
style = spanStyleOf(
color = BitwardenTheme.colorScheme.text.primary,
textStyle = BitwardenTheme.typography.bodyMedium,
),
),
confirmButtonText = stringResource(BitwardenString.yes),
dismissButtonText = stringResource(BitwardenString.no),
@@ -19,6 +19,7 @@ import com.bitwarden.vault.FieldView
import com.bitwarden.vault.IdentityView
import com.bitwarden.vault.LoginUriView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.vault.feature.attachments.util.isLargeFile
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
@@ -97,11 +98,7 @@ fun CipherView.toViewState(
title = requireNotNull(it.fileName),
displaySize = requireNotNull(it.sizeName),
url = requireNotNull(it.url),
isLargeFile = try {
requireNotNull(it.size).toLong() >= 10485760
} catch (_: NumberFormatException) {
false
},
isLargeFile = it.isLargeFile(),
isDownloadAllowed = isPremiumUser || this.organizationId != null,
)
}
@@ -54,7 +54,7 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
AttachmentsScreen(
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToPreview = { _, _, _ -> onNavigateToPreviewCalled = true },
onNavigateToPreview = { onNavigateToPreviewCalled = true },
)
}
}
@@ -81,6 +81,8 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
cipherId = "cipherId",
attachmentId = "attachmentId",
fileName = "file.png",
displaySize = "10 MB",
isLargeFile = true,
),
)
assertTrue(onNavigateToPreviewCalled)
@@ -337,6 +339,7 @@ private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content
id = "cipherId-1234",
title = "cool_file.png",
displaySize = "10 MB",
isLargeFile = true,
),
),
newAttachment = null,
@@ -137,6 +137,8 @@ class AttachmentsViewModelTest : BaseViewModelTest() {
cipherId = DEFAULT_STATE.cipherId,
attachmentId = DEFAULT_ATTACHMENT_ITEM.id,
fileName = DEFAULT_ATTACHMENT_ITEM.title,
displaySize = DEFAULT_ATTACHMENT_ITEM.displaySize,
isLargeFile = false,
),
awaitItem(),
)
@@ -850,6 +852,7 @@ private val DEFAULT_ATTACHMENT_ITEM: AttachmentsState.AttachmentItem =
id = "mockId-1",
title = "mockFileName-1",
displaySize = "mockSizeName-1",
isLargeFile = false,
)
private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content =
@@ -247,6 +247,35 @@ class PreviewAttachmentScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `confirm download dialog should display title and message`() {
composeTestRule.assertNoDialogExists()
mutableStateFlow.update {
it.copy(
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
displaySize = "2.89 MB",
),
)
}
composeTestRule
.onNodeWithText(text = "Download Attachment")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "This file is 2.89 MB. Would you like to download it?")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Yes")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
}
}
@Test
fun `NavigateToSelectAttachmentSaveLocation event should launch file chooser`() {
val fileName = "test.png"
@@ -265,6 +294,8 @@ private val DEFAULT_STATE = PreviewAttachmentState(
cipherId = "mockCipherId",
attachmentId = "mockAttachmentId",
fileName = DEFAULT_FILE_NAME,
displaySize = "2.89 MB",
isLargeFile = false,
isPreviewable = true,
viewState = PreviewAttachmentState.ViewState.Loading(),
dialogState = null,
@@ -304,6 +304,53 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
coVerify(exactly = 1) { fileManager.delete(mockFile) }
}
@Suppress("MaxLineLength")
@Test
fun `ConfirmDownloadClick should show loading dialog and emit NavigateToSelectAttachmentSaveLocation`() =
runTest {
val cipherView = createMockCipherView(number = 1)
mutableVaultItemStateFlow.value = DataState.Loaded(cipherView)
coEvery {
vaultRepository.downloadAttachment(
cipherView = cipherView,
attachmentId = DEFAULT_ATTACHMENT_ID,
)
} returns DownloadAttachmentResult.Success(mockFile)
val viewModel = createViewModel(initialState = NON_PREVIEWABLE_STATE)
viewModel.eventFlow.test {
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
assertEquals(
PreviewAttachmentEvent.NavigateToSelectAttachmentSaveLocation(
fileName = NON_PREVIEWABLE_STATE.fileName,
),
awaitItem(),
)
}
}
@Test
fun `DownloadClick should show confirmation dialog when file is large`() = runTest {
val cipherView = createMockCipherView(number = 1)
mutableVaultItemStateFlow.value = DataState.Loaded(cipherView)
coEvery {
vaultRepository.downloadAttachment(
cipherView = cipherView,
attachmentId = DEFAULT_ATTACHMENT_ID,
)
} returns DownloadAttachmentResult.Success(mockFile)
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(isLargeFile = true))
viewModel.trySendAction(PreviewAttachmentAction.DownloadClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
displaySize = DEFAULT_STATE.displaySize,
),
isLargeFile = true,
),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `DownloadClick should show loading dialog and emit NavigateToSelectAttachmentSaveLocation`() =
@@ -857,6 +904,8 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
cipherId = initialState?.cipherId ?: DEFAULT_CIPHER_ID,
attachmentId = initialState?.attachmentId ?: DEFAULT_ATTACHMENT_ID,
fileName = initialState?.fileName ?: DEFAULT_FILE_NAME,
displaySize = "2.89 MB",
isLargeFile = false,
)
},
)
@@ -865,11 +914,14 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
private const val DEFAULT_CIPHER_ID = "mockCipherId"
private const val DEFAULT_ATTACHMENT_ID = "mockAttachmentId"
private const val DEFAULT_FILE_NAME = "test.png"
private const val DEFAULT_DISPLAY_SIZE = "2.89 MB"
private val DEFAULT_STATE = PreviewAttachmentState(
cipherId = DEFAULT_CIPHER_ID,
attachmentId = DEFAULT_ATTACHMENT_ID,
fileName = DEFAULT_FILE_NAME,
displaySize = DEFAULT_DISPLAY_SIZE,
isLargeFile = false,
isPreviewable = true,
viewState = PreviewAttachmentState.ViewState.Loading(),
dialogState = null,
@@ -879,7 +931,9 @@ private val NON_PREVIEWABLE_STATE = PreviewAttachmentState(
cipherId = DEFAULT_CIPHER_ID,
attachmentId = DEFAULT_ATTACHMENT_ID,
fileName = "test.pdf",
displaySize = DEFAULT_DISPLAY_SIZE,
isPreviewable = false,
isLargeFile = false,
viewState = PreviewAttachmentState.ViewState.Error(
message = BitwardenString.preview_not_available_for_files.asText("PDF"),
),
@@ -23,6 +23,7 @@ class CipherViewExtensionsTest {
id = "mockId-1",
title = "mockFileName-1",
displaySize = "mockSizeName-1",
isLargeFile = false,
),
),
newAttachment = null,
@@ -41,6 +41,7 @@ import com.bitwarden.ui.util.onNodeWithTextAfterScroll
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
@@ -69,7 +70,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
private var onNavigateToMoveToOrganizationItemId: String? = null
private var onNavigateToAttachmentsId: String? = null
private var onNavigateToPasswordHistoryId: String? = null
private var onNavigateToPreviewAttachmentId: String? = null
private var onNavigateToPreviewAttachment: PreviewAttachmentRoute? = null
private val intentManager = mockk<IntentManager>(relaxed = true)
@@ -94,9 +95,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
},
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryId = it },
onNavigateToPreviewAttachment = { id, _, _ ->
onNavigateToPreviewAttachmentId = id
},
onNavigateToPreviewAttachment = { onNavigateToPreviewAttachment = it },
)
}
}
@@ -144,15 +143,23 @@ class VaultItemScreenTest : BitwardenComposeTest() {
@Test
fun `NavigateToPreviewAttachment event should invoke onNavigateToPreviewAttachment`() {
val cipherId = "cipherId1234"
val route = PreviewAttachmentRoute(
cipherId = "cipherId1234",
attachmentId = "attachmentId4321",
fileName = "fileName",
displaySize = "2.89 MB",
isLargeFile = false,
)
mutableEventFlow.tryEmit(
VaultItemEvent.NavigateToPreviewAttachment(
cipherId = cipherId,
cipherId = "cipherId1234",
attachmentId = "attachmentId4321",
fileName = "fileName",
displaySize = "2.89 MB",
isLargeFile = false,
),
)
assertEquals(cipherId, onNavigateToPreviewAttachmentId)
assertEquals(route, onNavigateToPreviewAttachment)
}
@Test
@@ -831,10 +838,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
.performClick()
composeTestRule
.onAllNodesWithText(
"This attachment is 11 MB in size. Are you sure you want to download it onto " +
"your device?",
)
.onAllNodesWithText("This file is 11 MB. Would you like to download it?")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
@@ -873,10 +877,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
.performClick()
composeTestRule
.onAllNodesWithText(
"This attachment is 11 MB in size. Are you sure you want to download it onto " +
"your device?",
)
.onAllNodesWithText("This file is 11 MB. Would you like to download it?")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
@@ -22,9 +22,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.bitwarden.ui.platform.base.util.toAnnotatedString
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
@@ -46,7 +48,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
* @param dismissTextColor The color of the dismiss text.
*/
@Composable
@Suppress("LongMethod")
fun BitwardenTwoButtonDialog(
title: String?,
message: String,
@@ -59,6 +60,50 @@ fun BitwardenTwoButtonDialog(
dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
) {
BitwardenTwoButtonDialog(
title = title,
message = message.toAnnotatedString(),
confirmButtonText = confirmButtonText,
dismissButtonText = dismissButtonText,
onConfirmClick = onConfirmClick,
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
confirmTextColor = confirmTextColor,
dismissTextColor = dismissTextColor,
dismissOnBackPress = dismissOnBackPress,
dismissOnClickOutside = dismissOnClickOutside,
)
}
/**
* Represents a Bitwarden-styled dialog with two buttons.
*
* @param title the optional title to show.
* @param message message to show.
* @param confirmButtonText text to show on confirm button.
* @param dismissButtonText text to show on dismiss button.
* @param onConfirmClick called when the confirm button is clicked.
* @param onDismissClick called when the dismiss button is clicked.
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
* tapping outside of it).
* @param confirmTextColor The color of the confirm text.
* @param dismissTextColor The color of the dismiss text.
*/
@Composable
@Suppress("LongMethod")
fun BitwardenTwoButtonDialog(
title: String?,
message: AnnotatedString,
confirmButtonText: String,
dismissButtonText: String,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
confirmTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
) {
Dialog(
onDismissRequest = onDismissRequest,
+3 -1
View File
@@ -189,7 +189,7 @@
<string name="attachments">Attachments</string>
<string name="unable_to_download_file">Unable to download file.</string>
<string name="downloading">Downloading…</string>
<string name="attachment_large_warning">This attachment is %1$s in size. Are you sure you want to download it onto your device?</string>
<string name="attachment_large_warning">This file is <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. Would you like to download it?</string>
<string name="authenticator_key">Authenticator key</string>
<string name="verification_code_totp">Verification code (TOTP)</string>
<string name="authenticator_key_added">Authenticator key added.</string>
@@ -1231,9 +1231,11 @@ Do you want to switch to this account?</string>
<string name="external_link">External link</string>
<string name="external_link_format" comment="Used for accessibility to indicate that tapping this item will leave the app">%1$s, External link</string>
<string name="preview_not_available_for_files">Preview unavailable for %1$s files. You can still download it to view on your device.</string>
<string name="this_file_is_too_large_to_preview">This file is too large to preview. You can still download it to view on your device.</string>
<string name="bitwarden_could_not_decrypt_this_file_so_the_preview_cannot_be_displayed">Bitwarden could not decrypt this file, so the preview cannot be displayed.</string>
<string name="preview_unavailable_for_this_file">Preview unavailable for this file. You can still download it to view on your device.</string>
<string name="preview_unavailable">Preview unavailable</string>
<string name="download_attachment">Download Attachment</string>
<string name="upgraded_to_premium">Upgraded to premium</string>
<string name="unlock_premium_features">Unlock more advanced features with a Premium plan.</string>
<string name="per_month">/ month</string>