Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

package com.x8bit.bitwarden.ui.platform.feature.vaultunlocked

import androidx.compose.runtime.Composable
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
Expand Down Expand Up @@ -56,6 +58,9 @@ import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.navigateToPreviewAttachment
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.previewAttachmentDestination
import com.x8bit.bitwarden.ui.vault.feature.media.VaultMediaViewerViewModel
import com.x8bit.bitwarden.ui.vault.feature.media.mediaViewerDestination
import com.x8bit.bitwarden.ui.vault.feature.media.navigateToMediaViewer
import com.x8bit.bitwarden.ui.vault.feature.importlogins.importLoginsScreenDestination
import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsScreen
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
Expand Down Expand Up @@ -92,6 +97,14 @@ fun NavGraphBuilder.vaultUnlockedGraph(
navigation<VaultUnlockedGraphRoute>(
startDestination = VaultUnlockedNavbarRoute,
) {
// Shared ViewModel scoped to this graph entry, ensuring the same
// instance is used by VaultItemScreen and MediaViewerScreen.
val getSharedMediaViewModel: @Composable () -> VaultMediaViewerViewModel = {
hiltViewModel(
navController.getBackStackEntry(VaultUnlockedGraphRoute),
)
}

vaultItemListingDestinationAsRoot(
onNavigateBack = { navController.popBackStack() },
onNavigateToVaultItemScreen = { navController.navigateToVaultItem(it) },
Expand Down Expand Up @@ -189,6 +202,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
onNavigateBack = { navController.popBackStack() },
)
vaultItemDestination(
getSharedMediaViewModel = getSharedMediaViewModel,
onNavigateBack = { navController.popBackStack() },
onNavigateToVaultEditItem = { navController.navigateToVaultAddEdit(it) },
onNavigateToMoveToOrganization = { vaultItemId, showOnlyCollections ->
Expand All @@ -210,6 +224,13 @@ fun NavGraphBuilder.vaultUnlockedGraph(
fileName = fileName,
)
},
onNavigateToMediaViewer = { cipherId, attachmentId, fileName ->
navController.navigateToMediaViewer(
cipherId = cipherId,
attachmentId = attachmentId,
fileName = fileName,
)
},
)
vaultQrCodeScanDestination(
onNavigateToManualCodeEntryScreen = {
Expand Down Expand Up @@ -283,6 +304,10 @@ fun NavGraphBuilder.vaultUnlockedGraph(
importLoginsScreenDestination(
onNavigateBack = { navController.popBackStack() },
)
mediaViewerDestination(
getSharedMediaViewModel = getSharedMediaViewModel,
onNavigateBack = { navController.popBackStack() },
)
previewAttachmentDestination(
onNavigateBack = { navController.popBackStack() },
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.x8bit.bitwarden.ui.vault.feature.item

import android.graphics.drawable.Drawable
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target

/**
* Glide [RequestListener] that signals the ViewModel to delete the decrypted
* temporary file as soon as the bitmap has been rendered into memory.
*
* This implements the core "burn-after-reading" security mechanism:
* - [onResourceReady]: Bitmap is in RAM β†’ delete file from disk.
* - [onLoadFailed]: Loading failed β†’ still delete file from disk.
*
* @param attachmentId The unique attachment ID for cleanup targeting.
* @param onComplete Callback to signal the ViewModel (typically
* [VaultMediaViewerViewModel.onBitmapRenderComplete]).
*/
class BurnAfterReadingListener(
private val attachmentId: String,
private val onComplete: (String) -> Unit,
) : RequestListener<Drawable> {

override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean,
): Boolean {
// Bitmap is now in Glide's LRU memory cache. Delete the source file.
onComplete(attachmentId)
return false
}

override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean,
): Boolean {
// Even on failure, clean up the temp file.
onComplete(attachmentId)
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
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.media.MediaPreviewState
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.util.shortName

Expand All @@ -44,6 +45,7 @@ import com.x8bit.bitwarden.ui.vault.util.shortName
fun VaultItemCardContent(
commonState: VaultItemState.ViewState.Content.Common,
cardState: VaultItemState.ViewState.Content.ItemType.Card,
mediaInlineStates: Map<String, MediaPreviewState>,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -275,35 +277,93 @@ fun VaultItemCardContent(
}

commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item(key = "attachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
val imageAttachments = attachments.filter { it.isImageType }
val nonImageAttachments = attachments.filter { !it.isImageType }

if (imageAttachments.isNotEmpty()) {
item(key = "imageAttachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(
items = imageAttachments,
key = { index, _ -> "imageAttachment_$index" },
) { index, attachmentItem ->
ImageAttachmentItemContent(
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.animateItem(),
attachmentItem = attachmentItem,
previewState = mediaInlineStates[attachmentItem.id]
?: MediaPreviewState.Masked,
cardStyle = imageAttachments.toListItemCardStyle(
index = index,
),
onAttachmentPreviewClick = { id ->
val item = imageAttachments.firstOrNull { it.id == id }
if (item != null) {
vaultCommonItemTypeHandlers
.onAttachmentPreviewClick(item)
}
},
onAttachmentImageViewClick = vaultCommonItemTypeHandlers
.onAttachmentImageViewClick,
onBitmapRenderComplete = vaultCommonItemTypeHandlers
.onBitmapRenderComplete,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers
.onUpgradeToPremiumClick,
)
}
}
itemsIndexed(
items = attachments,
key = { index, _ -> "attachment_$index" },
) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)

if (nonImageAttachments.isNotEmpty()) {
if (imageAttachments.isEmpty()) {
item(key = "attachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
itemsIndexed(
items = nonImageAttachments,
key = { index, _ -> "attachment_$index" },
) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onAttachmentPreviewClick = vaultCommonItemTypeHandlers
.onAttachmentPreviewClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers
.onUpgradeToPremiumClick,
cardStyle = nonImageAttachments.toListItemCardStyle(
index = index,
),
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.component.CustomField
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.media.MediaPreviewState

/**
* The top level content UI state for the [VaultItemScreen] when viewing a Identity cipher.
Expand All @@ -44,6 +45,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeH
fun VaultItemIdentityContent(
identityState: VaultItemState.ViewState.Content.ItemType.Identity,
commonState: VaultItemState.ViewState.Content.Common,
mediaInlineStates: Map<String, MediaPreviewState>,
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -336,35 +338,93 @@ fun VaultItemIdentityContent(
}

commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item(key = "attachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
val imageAttachments = attachments.filter { it.isImageType }
val nonImageAttachments = attachments.filter { !it.isImageType }

if (imageAttachments.isNotEmpty()) {
item(key = "imageAttachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
itemsIndexed(
items = imageAttachments,
key = { index, _ -> "imageAttachment_$index" },
) { index, attachmentItem ->
ImageAttachmentItemContent(
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.animateItem(),
attachmentItem = attachmentItem,
previewState = mediaInlineStates[attachmentItem.id]
?: MediaPreviewState.Masked,
cardStyle = imageAttachments.toListItemCardStyle(
index = index,
),
onAttachmentPreviewClick = { id ->
val item = imageAttachments.firstOrNull { it.id == id }
if (item != null) {
vaultCommonItemTypeHandlers
.onAttachmentPreviewClick(item)
}
},
onAttachmentImageViewClick = vaultCommonItemTypeHandlers
.onAttachmentImageViewClick,
onBitmapRenderComplete = vaultCommonItemTypeHandlers
.onBitmapRenderComplete,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers
.onUpgradeToPremiumClick,
)
}
}
itemsIndexed(
items = attachments,
key = { index, _ -> "attachment_$index" },
) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onAttachmentPreviewClick = vaultCommonItemTypeHandlers.onAttachmentPreviewClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers.onUpgradeToPremiumClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)

if (nonImageAttachments.isNotEmpty()) {
if (imageAttachments.isEmpty()) {
item(key = "attachmentsHeader") {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
itemsIndexed(
items = nonImageAttachments,
key = { index, _ -> "attachment_$index" },
) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
onAttachmentPreviewClick = vaultCommonItemTypeHandlers
.onAttachmentPreviewClick,
onUpgradeToPremiumClick = vaultCommonItemTypeHandlers
.onUpgradeToPremiumClick,
cardStyle = nonImageAttachments.toListItemCardStyle(
index = index,
),
)
}
}
}

Expand Down
Loading
Loading