Skip to content
Merged
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
148 changes: 148 additions & 0 deletions app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.ui.fileactions

import androidx.annotation.IdRes
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.editimage.EditImageActivity
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.utils.EditorUtils
import com.nextcloud.utils.ShortcutUtil
import com.nextcloud.utils.extensions.handle
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.ui.activity.TextEditorWebView
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
import com.owncloud.android.ui.dialog.RenameFileDialogFragment
import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment
import com.owncloud.android.ui.fragment.FileDetailFragment
import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import javax.inject.Inject

@Suppress("LongParameterList")
class FileActionHandler @Inject constructor(
private val backgroundJobManager: BackgroundJobManager,
private val accountManager: UserAccountManager,
private val editorUtils: EditorUtils,
private val shortcutUtil: ShortcutUtil,
private val syncedFolderProvider: SyncedFolderProvider,
private val viewThemeUtils: ViewThemeUtils
) {
fun handle(@IdRes itemId: Int, files: Collection<OCFile>, host: FileActionHost): Boolean =
files.isNotEmpty() && if (files.size == 1) {
file(itemId, files.first(), host)
} else {
files(itemId, files, host)
}

@Suppress("CyclomaticComplexMethod")
private fun file(@IdRes itemId: Int, file: OCFile, host: FileActionHost): Boolean {
when (itemId) {
R.id.action_send_share_file -> host.containerActivity.showDetails(file, SHARING_TAB)

R.id.action_rename_file -> RenameFileDialogFragment.newInstance(file, host.parentFolder)
.show(host.fragmentManager, FileDetailFragment.FTAG_RENAME_FILE)

R.id.action_open_in_web_editor -> TextEditorWebView.startTextEditor(file, host.context)

R.id.action_edit -> edit(file, host)

R.id.action_download_file, R.id.action_sync_file -> sync(file, host)

R.id.action_sync_all_files -> syncFolder(file, host)

R.id.action_pin_to_homescreen ->
shortcutUtil.addShortcutToHomescreen(file, viewThemeUtils, accountManager.user, syncedFolderProvider)

R.id.action_retry -> backgroundJobManager.startOfflineOperations()

else -> return host.helper?.handle(itemId, file, host.view) == true || files(itemId, listOf(file), host)
}

return true
}

private fun files(@IdRes itemId: Int, files: Collection<OCFile>, host: FileActionHost): Boolean {
when (itemId) {
R.id.action_remove_file -> RemoveFilesDialogFragment.newInstance(ArrayList(files), host.actionMode)
.show(host.fragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)

R.id.action_cancel_sync -> files.forEach {
host.fda?.cancelTransference(it)
}

R.id.action_favorite -> setFavorite(files, true, host)

R.id.action_unset_favorite -> setFavorite(files, false, host)

else -> return host.helper?.handle(itemId, files, host.view, backgroundJobManager) == true
}

return true
}

private fun setFavorite(files: Collection<OCFile>, isFavorite: Boolean, host: FileActionHost) {
host.helper?.toggleFavoriteFiles(files, isFavorite)
host.onFavoriteChanged(isFavorite)
}

private fun edit(file: OCFile, host: FileActionHost) {
val context = host.context ?: return
when {
editorUtils.isEditorAvailable(accountManager.user, file.mimeType) ->
TextEditorWebView.startTextEditor(file, context)

EditImageActivity.canBePreviewed(file) ->
host.fda?.startImageEditor(file)

else -> host.helper?.openFileAsRichDocument(file, context)
}
}

private fun sync(file: OCFile, host: FileActionHost) {
host.fileActivity?.showSyncLoadingDialog(file.isFolder)
host.helper?.syncFileOrFolder(file)
}

private fun syncFolder(folder: OCFile, host: FileActionHost) {
if (!FileStorageUtils.checkIfEnoughSpace(folder)) {
SyncFileNotEnoughSpaceDialogFragment
.newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice())
.show(host.fragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
return
}

ConfirmationDialogFragment.newInstance(
R.string.sync_all_action_dialog_description,
null,
R.string.sync_all_action_dialog_title,
R.drawable.ic_sync_all,
R.string.common_ok,
R.string.common_cancel,
NO_NEUTRAL_BUTTON
).run {
isCancelable = false
setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener {
override fun onConfirmation(callerTag: String?) {
host.helper?.syncFolderIncludingNestedFiles(folder)
}
override fun onNeutral(callerTag: String?) = Unit
override fun onCancel(callerTag: String?) = Unit
})
show(host.fragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
}
}

companion object {
private const val SHARING_TAB = 1
private const val NO_NEUTRAL_BUTTON = -1
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/nextcloud/ui/fileactions/FileActionHost.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.ui.fileactions

import android.content.Context
import android.view.ActionMode
import android.view.View
import androidx.fragment.app.FragmentManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.FileActivity
import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.fragment.FileFragment
import com.owncloud.android.ui.helpers.FileOperationsHelper

@Suppress("LongParameterList")
class FileActionHost @JvmOverloads constructor(
val containerActivity: FileFragment.ContainerActivity,
val fragmentManager: FragmentManager,
val view: View?,
val parentFolder: OCFile?,
val actionMode: ActionMode? = null,
val onFavoriteChanged: (isFavorite: Boolean) -> Unit = {}
) {
val helper: FileOperationsHelper?
get() = containerActivity.fileOperationsHelper

val fileActivity: FileActivity?
get() = containerActivity as? FileActivity

val fda: FileDisplayActivity?
get() = containerActivity as? FileDisplayActivity

val context: Context?
get() = view?.context
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import android.view.View
import androidx.annotation.IdRes
import com.nextcloud.client.jobs.BackgroundJobManager
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.helpers.FileOperationsHelper

fun FileOperationsHelper.handle(@IdRes itemId: Int, file: OCFile, view: View?): Boolean {
when (itemId) {
R.id.action_send_file -> sendShareFile(file, true)
R.id.action_open_file_with -> openFile(file)
R.id.action_stream_media -> streamMediaFile(file)
R.id.action_set_as_wallpaper -> setPictureAs(file, view)
R.id.action_encrypted -> toggleEncryption(file, true)
R.id.action_unset_encrypted -> toggleEncryption(file, false)
R.id.action_lock_file -> toggleFileLock(file, true)
R.id.action_unlock_file -> toggleFileLock(file, false)
else -> return false
}

return true
}

fun FileOperationsHelper.handle(
@IdRes itemId: Int,
files: Collection<OCFile>,
view: View?,
backgroundJobManager: BackgroundJobManager
): Boolean {
when (itemId) {
R.id.action_export_file -> exportFiles(files, view?.context, view, backgroundJobManager)
R.id.action_add_to_album -> addFileToAlbum(files)
else -> return false
}

return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.User
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.di.ViewModelFactory
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.download.FileDownloadHelper
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.repository.ClientRepository
import com.nextcloud.ui.fileDetail.FileDetailPreview
import com.nextcloud.ui.fileactions.FileAction
import com.nextcloud.ui.fileactions.FileActionHandler
import com.nextcloud.ui.fileactions.FileActionHost
import com.nextcloud.ui.fileactions.FileActionsBottomSheet
import com.nextcloud.ui.tags.TagManagementBottomSheet
import com.nextcloud.utils.HumanReadableFormatter
Expand All @@ -66,13 +67,10 @@ import com.owncloud.android.lib.resources.shares.OCShare
import com.owncloud.android.lib.resources.shares.ShareType
import com.owncloud.android.lib.resources.tags.Tag
import com.owncloud.android.ui.activity.DrawerActivity
import com.owncloud.android.ui.activity.FileActivity
import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.activity.ToolbarActivity
import com.owncloud.android.ui.adapter.FileDetailTabAdapter
import com.owncloud.android.ui.adapter.progressListener.DownloadProgressListener
import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
import com.owncloud.android.ui.dialog.RenameFileDialogFragment
import com.owncloud.android.ui.events.FavoriteEvent
import com.owncloud.android.ui.fragment.filedetail.FileDetailFragmentViewModel
import com.owncloud.android.ui.helpers.FileOperationsHelper
Expand Down Expand Up @@ -107,7 +105,7 @@ class FileDetailFragment :
lateinit var viewThemeUtils: ViewThemeUtils

@Inject
lateinit var backgroundJobManager: BackgroundJobManager
lateinit var fileActionHandler: FileActionHandler

@Inject
lateinit var folderThumbnailGenerator: FolderThumbnailGenerator
Expand Down Expand Up @@ -460,41 +458,15 @@ class FileDetailFragment :

private fun onFileActionSelected(@IdRes itemId: Int) {
val file = file ?: return
when (itemId) {
R.id.action_send_file -> fileOperationsHelper?.sendShareFile(file, true)

R.id.action_open_file_with -> fileOperationsHelper?.openFile(file)

R.id.action_remove_file -> RemoveFilesDialogFragment.newInstance(file)
.show(parentFragmentManager, FTAG_CONFIRMATION)

R.id.action_rename_file -> RenameFileDialogFragment.newInstance(file, parentFolder)
.show(parentFragmentManager, FTAG_RENAME_FILE)

R.id.action_cancel_sync -> (containerActivity as? FileDisplayActivity)?.cancelTransference(file)

R.id.action_download_file, R.id.action_sync_file -> syncFile(file)

R.id.action_export_file -> fileOperationsHelper?.exportFiles(
arrayListOf(file),
context,
view,
backgroundJobManager
)

R.id.action_set_as_wallpaper -> fileOperationsHelper?.setPictureAs(file, view)

R.id.action_retry -> backgroundJobManager.startOfflineOperations()

R.id.action_favorite -> setFavorite(file, true)

R.id.action_unset_favorite -> setFavorite(file, false)
}
}

private fun syncFile(file: OCFile) {
(containerActivity as? FileActivity)?.showSyncLoadingDialog(file.isFolder)
fileOperationsHelper?.syncFileOrFolder(file)
val containerActivity = containerActivity ?: return
val host = FileActionHost(
containerActivity,
parentFragmentManager,
view,
parentFolder,
onFavoriteChanged = ::setFavoriteIconStatus
)
fileActionHandler.handle(itemId, listOf(file), host)
}

private fun toggleDetailedTimestamp(file: OCFile) {
Expand All @@ -515,11 +487,6 @@ class FileDetailFragment :
// endregion

// region Favorite
private fun setFavorite(file: OCFile, isFavorite: Boolean) {
fileOperationsHelper?.toggleFavoriteFile(file, isFavorite)
setFavoriteIconStatus(isFavorite)
}

private fun setFavoriteIconStatus(isFavorite: Boolean) {
val binding = binding ?: return
val (iconRes, descriptionRes) = if (isFavorite) {
Expand Down Expand Up @@ -745,7 +712,6 @@ class FileDetailFragment :

companion object {
private val TAG: String = FileDetailFragment::class.java.simpleName
private const val FTAG_CONFIRMATION = "REMOVE_CONFIRMATION_FRAGMENT"
private const val FTAG_FILE_ACTIONS = "actions"
private const val FTAG_TAG_MANAGEMENT = "tag_management"
const val FTAG_RENAME_FILE = "RENAME_FILE_FRAGMENT"
Expand Down
Loading
Loading