mirror of
https://github.com/bitwarden/android.git
synced 2026-08-16 03:01:21 -05:00
PM-34544: bug: Handle large attachments in preview (#6757)
This commit is contained in:
+2
-14
@@ -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 = {
|
||||
|
||||
+2
-5
@@ -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(
|
||||
|
||||
+11
-2
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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()
|
||||
|
||||
/**
|
||||
|
||||
+8
-8
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+27
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+46
-4
@@ -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.
|
||||
*/
|
||||
|
||||
+15
@@ -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
|
||||
}
|
||||
+1
@@ -19,6 +19,7 @@ fun CipherView.toViewState(): AttachmentsState.ViewState.Content =
|
||||
id = id,
|
||||
title = it.fileName.orEmpty(),
|
||||
displaySize = it.sizeName.orEmpty(),
|
||||
isLargeFile = it.isLargeFile(),
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
|
||||
+2
-5
@@ -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()
|
||||
|
||||
/**
|
||||
|
||||
+10
-8
@@ -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),
|
||||
|
||||
+2
-5
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-1
@@ -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,
|
||||
|
||||
+3
@@ -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 =
|
||||
|
||||
+31
@@ -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,
|
||||
|
||||
+54
@@ -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"),
|
||||
),
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ class CipherViewExtensionsTest {
|
||||
id = "mockId-1",
|
||||
title = "mockFileName-1",
|
||||
displaySize = "mockSizeName-1",
|
||||
isLargeFile = false,
|
||||
),
|
||||
),
|
||||
newAttachment = null,
|
||||
|
||||
+16
-15
@@ -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()
|
||||
|
||||
|
||||
+46
-1
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user