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 @@ -135,6 +135,17 @@ fun Activity.goToUrl(
}
}

/**
* Vaults (folders and spaces) are encrypted by the OpenCloud Web vault app, so they can only be opened in the browser.
*/
fun Activity.openVaultInBrowser(url: String?) {
if (url.isNullOrBlank()) {
showMessageInSnackbar(message = getString(R.string.vault_link_unavailable))
} else {
goToUrl(url)
}
}

fun Activity.openPrivacyPolicy() {
val urlPrivacyPolicy = getString(R.string.url_privacy_policy)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import eu.opencloud.android.domain.files.usecases.MoveFileUseCase
import eu.opencloud.android.domain.files.usecases.RemoveFileUseCase
import eu.opencloud.android.domain.files.usecases.RenameFileUseCase
import eu.opencloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase
import eu.opencloud.android.domain.spaces.usecases.GetSpaceByIdForAccountUseCase
import eu.opencloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
import eu.opencloud.android.presentation.authentication.AccountUtils
import eu.opencloud.android.presentation.documentsprovider.cursors.FileCursor
Expand Down Expand Up @@ -288,6 +289,12 @@ class DocumentsStorageProvider : DocumentsProvider() {
// Folder id is not null, so this is a regular folder
resultCursor = FileCursor(projection)

if (isVault(getFileByIdOrException(folderId.toInt()))) {
// Vault contents are encrypted by the web vault app, so they are neither listed nor synced
resultCursor.setNotificationUri(context?.contentResolver, toNotifyUri(toUri(parentDocumentId)))
return resultCursor
}

// Create result cursor before syncing folder again, in order to enable faster loading
getFolderContent(folderId.toInt()).forEach { file -> resultCursor.addFile(file) }

Expand Down Expand Up @@ -406,6 +413,7 @@ class DocumentsStorageProvider : DocumentsProvider() {
): String {
Timber.d("Create Document ParentID $parentDocumentId Type $mimeType DisplayName $displayName")
val parentDocument = getFileByIdOrException(parentDocumentId.toInt())
checkIsNotVault(parentDocument)

return if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
createFolder(parentDocument, displayName)
Expand Down Expand Up @@ -446,6 +454,8 @@ class DocumentsStorageProvider : DocumentsProvider() {

val sourceFile = getFileByIdOrException(sourceDocumentId.toInt())
val targetParentFile = getFileByIdOrException(targetParentDocumentId.toInt())
checkIsNotVault(sourceFile)
checkIsNotVault(targetParentFile)

val copyFileUseCase: CopyFileUseCase by inject()

Expand Down Expand Up @@ -476,6 +486,8 @@ class DocumentsStorageProvider : DocumentsProvider() {

val sourceFile = getFileByIdOrException(sourceDocumentId.toInt())
val targetParentFile = getFileByIdOrException(targetParentDocumentId.toInt())
checkIsNotVault(sourceFile)
checkIsNotVault(targetParentFile)

val moveFileUseCase: MoveFileUseCase by inject()

Expand Down Expand Up @@ -770,7 +782,7 @@ class DocumentsStorageProvider : DocumentsProvider() {
folderContent.forEach {
if (it.fileName.contains(query)) {
result.add(it)
if (it.isFolder) result.addAll(findFiles(it, query))
if (it.isFolder && !it.isVault) result.addAll(findFiles(it, query))
}
}
return result
Expand All @@ -786,6 +798,24 @@ class DocumentsStorageProvider : DocumentsProvider() {

private fun toUri(documentId: String): Uri = Uri.parse(documentId)

/**
* Vaults are encrypted by the OpenCloud Web vault app: folders carrying the vault marker,
* and the root folders of vault spaces. Their contents can't be used by this client.
*/
private fun isVault(file: OCFile): Boolean {
if (file.isVault) return true
if (file.remotePath != ROOT_PATH || file.spaceId == null) return false

val getSpaceByIdForAccountUseCase: GetSpaceByIdForAccountUseCase by inject()
return getSpaceByIdForAccountUseCase(
GetSpaceByIdForAccountUseCase.Params(accountName = file.owner, spaceId = file.spaceId)
)?.isVault == true
}

private fun checkIsNotVault(file: OCFile) {
if (isVault(file)) throw UnsupportedOperationException("${file.remotePath} is an encrypted vault")
}

private fun getFileByIdOrException(id: Int): OCFile {
val getFileByIdUseCase: GetFileByIdUseCase by inject()
val result = getFileByIdUseCase(GetFileByIdUseCase.Params(id.toLong()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ class FileCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAUL
val imagePath = if (file.isImage && file.isAvailableLocally) file.storagePath else null
var flags = if (imagePath != null) Document.FLAG_SUPPORTS_THUMBNAIL else 0

flags = flags or Document.FLAG_SUPPORTS_DELETE
flags = flags or Document.FLAG_SUPPORTS_RENAME
flags = flags or Document.FLAG_SUPPORTS_COPY
flags = flags or Document.FLAG_SUPPORTS_MOVE
if (!file.isVault) { // Vault folders are encrypted by the web vault app, so no action can be performed on them
flags = flags or Document.FLAG_SUPPORTS_DELETE
flags = flags or Document.FLAG_SUPPORTS_RENAME
flags = flags or Document.FLAG_SUPPORTS_COPY
flags = flags or Document.FLAG_SUPPORTS_MOVE

if (mimeType != Document.MIME_TYPE_DIR) { // If it is a file
flags = flags or Document.FLAG_SUPPORTS_WRITE
} else if (file.hasAddFilePermission && file.hasAddSubdirectoriesPermission) { // If it is a folder with writing permissions
flags = flags or Document.FLAG_DIR_SUPPORTS_CREATE
if (mimeType != Document.MIME_TYPE_DIR) { // If it is a file
flags = flags or Document.FLAG_SUPPORTS_WRITE
} else if (file.hasAddFilePermission && file.hasAddSubdirectoriesPermission) { // If it is a folder with writing permissions
flags = flags or Document.FLAG_DIR_SUPPORTS_CREATE
}
}

newRow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class SpaceCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAU
}

fun addSpace(space: OCSpace, rootFolder: OCFile, context: Context?) {
val flags = if (rootFolder.hasAddFilePermission && rootFolder.hasAddSubdirectoriesPermission) {
// Vault spaces are encrypted by the web vault app, so nothing can be created in them
val flags = if (!space.isVault && rootFolder.hasAddFilePermission && rootFolder.hasAddSubdirectoriesPermission) {
Document.FLAG_DIR_SUPPORTS_CREATE
} else {
0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ class FileListAdapter(
return checkedItems
}

// Vault folders are encrypted by the web vault app, so no action can be performed on them
override fun isSelectable(position: Int): Boolean =
(files.getOrNull(position) as? OCFileWithSyncInfo)?.file?.isVault == false

fun selectAll() {
// Last item on list is the footer, so that element must be excluded from selection
selectAll(totalItems = selectableItemCount())
Expand Down Expand Up @@ -273,7 +277,7 @@ class FileListAdapter(
)
}
val checkBoxV = holder.itemView.findViewById<ImageView>(R.id.custom_checkbox).apply {
isVisible = hasActiveSelection
isVisible = hasActiveSelection && !file.isVault
}

if (isSelected(position)) {
Expand Down Expand Up @@ -341,7 +345,7 @@ class FileListAdapter(
it.Filename.text = file.fileName
it.fileListSize.text = DisplayUtils.bytesToHumanReadable(file.length, context, true)
it.fileListLastMod.text = DisplayUtils.getRelativeTimestamp(context, file.modificationTimestamp)
it.threeDotMenu.isVisible = !hasActiveSelection
it.threeDotMenu.isVisible = !hasActiveSelection && !file.isVault // Vaults only open in the browser
it.threeDotMenu.contentDescription = context.getString(R.string.content_description_file_operations, file.fileName)
if (fileListOption.isAvailableOffline() || (fileListOption.isSharedByLink() && fileWithSyncInfo.space == null)) {
it.spacePathLine.path.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import eu.opencloud.android.extensions.collectLatestLifecycleFlow
import eu.opencloud.android.extensions.filterMenuOptions
import eu.opencloud.android.extensions.parseError
import eu.opencloud.android.extensions.sendDownloadedFilesByShareSheet
import eu.opencloud.android.extensions.openVaultInBrowser
import eu.opencloud.android.extensions.SNACKBAR_DURATION_ERROR_MS
import eu.opencloud.android.extensions.showErrorInSnackbar
import eu.opencloud.android.extensions.showMessageInSnackbar
Expand Down Expand Up @@ -1570,15 +1571,18 @@ class MainFileListFragment : Fragment(),

val ocFile = ocFileWithSyncInfo.file

if (ocFile.isFolder) {
if (ocFile.isVault) {
// Vault folders are encrypted by the web vault app, so they are opened in the browser instead of being listed
requireActivity().openVaultInBrowser(ocFile.privateLink)
} else if (ocFile.isFolder) {
mainFileListViewModel.updateFolderToDisplay(ocFile)
} else { // Click on a file
fileActions?.onFileClicked(ocFile)
}
}

override fun onLongItemClick(position: Int): Boolean {
if (isPickingAFolder()) return false
if (isPickingAFolder() || !fileListAdapter.isSelectable(position)) return false

if (actionMode == null) {
actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(actionModeCallback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ abstract class SelectableAdapter<VH : RecyclerView.ViewHolder?> :
fun isSelected(position: Int): Boolean =
getSelectedItems().contains(position)

/**
* Indicates if the item at position position can be selected at all
*/
open fun isSelectable(position: Int): Boolean = true

/**
* Toggle the selection status of the item at a given position
* @param position Position of the item to toggle the selection status for
*/
fun toggleSelection(position: Int) {
if (!isSelectable(position)) return
if (selectedItems[position, false]) {
selectedItems.delete(position)
} else {
Expand Down Expand Up @@ -81,6 +87,7 @@ abstract class SelectableAdapter<VH : RecyclerView.ViewHolder?> :
*/
fun toggleSelectionInBulk(totalItems: Int) {
for (i in 0 until totalItems) {
if (!isSelectable(i)) continue
if (selectedItems[i, false]) {
selectedItems.delete(i)
} else {
Expand All @@ -92,6 +99,7 @@ abstract class SelectableAdapter<VH : RecyclerView.ViewHolder?> :

fun selectAll(totalItems: Int) {
for (i in 0 until totalItems) {
if (!isSelectable(i)) continue
selectedItems.put(i, true)
}
notifyDataSetChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import eu.opencloud.android.databinding.SpacesListFragmentBinding
import eu.opencloud.android.domain.files.model.FileListOption
import eu.opencloud.android.domain.spaces.model.OCSpace
import eu.opencloud.android.extensions.collectLatestLifecycleFlow
import eu.opencloud.android.extensions.openVaultInBrowser
import eu.opencloud.android.extensions.showErrorInSnackbar
import eu.opencloud.android.extensions.toDrawableRes
import eu.opencloud.android.extensions.toSubtitleStringRes
Expand Down Expand Up @@ -134,6 +135,11 @@ class SpacesListFragment : SpacesListAdapter.SpacesListAdapterListener, Fragment
}

override fun onItemClick(ocSpace: OCSpace) {
if (ocSpace.isVault) {
// Vault spaces are encrypted by the web vault app, so they are opened in the browser instead of being listed
requireActivity().openVaultInBrowser(ocSpace.webUrl)
return
}
spacesListViewModel.getRootFileForSpace(ocSpace)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import eu.opencloud.android.extensions.isDownloadPending
import eu.opencloud.android.extensions.manageOptionLockSelected
import eu.opencloud.android.extensions.observeWorkerTillItFinishes
import eu.opencloud.android.extensions.openOCFile
import eu.opencloud.android.extensions.openVaultInBrowser
import eu.opencloud.android.extensions.parseError
import eu.opencloud.android.extensions.sendDownloadedFilesByShareSheet
import eu.opencloud.android.extensions.SNACKBAR_DURATION_ERROR_MS
Expand Down Expand Up @@ -2119,6 +2120,11 @@ class FileDisplayActivity : FileActivity(),
}

private fun manageItem(file: OCFile) {
if (file.isVault) {
// Vault folders are encrypted by the web vault app, so they are opened in the browser instead of being listed
openVaultInBrowser(file.privateLink)
return
}
setFile(file)
account = eu.opencloud.android.presentation.authentication.AccountUtils.getOpenCloudAccountByName(this, file.owner)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,10 @@ public void onItemClick(AdapterView<?> parent, View view, int position, long id)
if (files.size() < position) {
throw new IndexOutOfBoundsException("Incorrect item selected");
}
if (files.get(position).isFolder()) {
if (files.get(position).isVault()) {
// Vault folders are encrypted by the web vault app, so they are opened in the browser instead of being listed
ActivityExtKt.openVaultInBrowser(this, files.get(position).getPrivateLink());
} else if (files.get(position).isFolder()) {
OCFile folderToEnter = files.get(position);
startSyncFolderOperation(folderToEnter);
mParents.push(folderToEnter.getFileName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class FilterFileMenuOptionsUseCase(
if (params.displaySelectInverse) {
optionsToShow.add(FileMenuOption.SELECT_INVERSE)
}
if (files.any { it.isVault }) {
// Vault folders are encrypted by the web vault app, so no action can be performed on them
return optionsToShow
}
if (!params.onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed &&
state.resharingAllowed && state.isPersonalSpace &&
state.hasResharePermission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class SynchronizeFolderUseCase(
val remotePath = params.remotePath
val accountName = params.accountName

// Vault folders are encrypted by the web vault app, so their contents are never listed or synced
if (fileRepository.getFileByRemotePath(remotePath, accountName, params.spaceId)?.isVault == true) return

val folderContent = fileRepository.refreshFolder(
remotePath = remotePath,
accountName = accountName,
Expand Down Expand Up @@ -68,7 +71,8 @@ class SynchronizeFolderUseCase(
}

private fun shouldSyncFolder(syncMode: SyncFolderMode, ocFolder: OCFile) =
syncMode.isOneOf(REFRESH_FOLDER_RECURSIVELY, SYNC_FOLDER_RECURSIVELY) || syncMode == SYNC_CONTENTS && ocFolder.isAvailableOffline
!ocFolder.isVault &&
(syncMode.isOneOf(REFRESH_FOLDER_RECURSIVELY, SYNC_FOLDER_RECURSIVELY) || syncMode == SYNC_CONTENTS && ocFolder.isAvailableOffline)

private fun shouldSyncFile(syncMode: SyncFolderMode, ocFile: OCFile) =
syncMode == SYNC_FOLDER_RECURSIVELY || (syncMode == SYNC_CONTENTS && (ocFile.isAvailableLocally || ocFile.isAvailableOffline))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ class AccountDiscoveryWorker(
}

// Then we discover the root of the rest of spaces
val spacesWithoutPersonal = spaces.filterNot { it.isPersonal }
// Vault spaces are encrypted by the web vault app, so their contents are not discovered
val spacesWithoutPersonal = spaces.filterNot { it.isPersonal || it.isVault }
spacesWithoutPersonal.forEach { space ->
// Create the root file for each space
val rootFolderForSpace =
Expand Down
1 change: 1 addition & 0 deletions opencloudApp/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@
<string name="share_via_link_empty_password">The password is empty.</string>
<string name="private_link_info">Private link: Only people who have access to the file/folder can use it. Use it as a permanent link for yourself or to point others to files within shares.</string>
<string name="file_private_link_error">Private link is not available for this file.</string>
<string name="vault_link_unavailable">This encrypted vault can only be opened in the browser, but no link is available for it.</string>
<string name="download_notification_channel_name">Downloads</string>
<string name="download_notification_channel_description">See the download file name and progress.</string>
<string name="upload_notification_channel_name">Uploads</string>
Expand Down
Loading
Loading