diff --git a/changelog/unreleased/4289 b/changelog/unreleased/4289 new file mode 100644 index 00000000000..65b2c8f3be7 --- /dev/null +++ b/changelog/unreleased/4289 @@ -0,0 +1,6 @@ +Enhancement: Correct "Local only" option in remove dialog + +"Local only" option in remove dialog will only be shown if checking selected files and folders recursively, at least one file is available locally. + +https://github.com/owncloud/android/issues/3936 +https://github.com/owncloud/android/pull/4289 diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 968a21e3bb8..4fe51ca8d14 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -60,6 +60,7 @@ import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase import com.owncloud.android.domain.files.usecases.GetFileWithSyncInfoByIdUseCase import com.owncloud.android.domain.files.usecases.GetFolderContentAsStreamUseCase import com.owncloud.android.domain.files.usecases.GetFolderContentUseCase +import com.owncloud.android.domain.files.usecases.IsAnyFileAvailableLocallyUseCase import com.owncloud.android.domain.files.usecases.GetFolderImagesUseCase import com.owncloud.android.domain.files.usecases.GetPersonalRootFolderForAccountUseCase import com.owncloud.android.domain.files.usecases.GetSearchFolderContentUseCase @@ -166,6 +167,7 @@ val useCaseModule = module { factoryOf(::GetFolderContentAsStreamUseCase) factoryOf(::GetFolderContentUseCase) factoryOf(::GetFolderImagesUseCase) + factoryOf(::IsAnyFileAvailableLocallyUseCase) factoryOf(::GetPersonalRootFolderForAccountUseCase) factoryOf(::GetSearchFolderContentUseCase) factoryOf(::GetSharedByLinkForAccountAsStreamUseCase) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt index bdcb934fda2..8a3a01813da 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt @@ -30,6 +30,7 @@ import com.owncloud.android.domain.utils.Event import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.providers.ContextProvider import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -117,6 +118,43 @@ object ViewModelExt : KoinComponent { } } + fun ViewModel.runUseCaseWithResult( + coroutineDispatcher: CoroutineDispatcher, + requiresConnection: Boolean = true, + showLoading: Boolean = false, + sharedFlow: MutableSharedFlow>, + useCase: BaseUseCaseWithResult, + useCaseParams: Params, + postSuccess: Boolean = true, + postSuccessWithData: Boolean = true + ) { + viewModelScope.launch(coroutineDispatcher) { + if (showLoading) { + sharedFlow.emit(UIResult.Loading()) + } + + // If use case requires connection and is not connected, it is not needed to execute use case + if (requiresConnection and !contextProvider.isConnected()) { + sharedFlow.emit(UIResult.Error(error = NoNetworkConnectionException())) + Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection") + return@launch + } + + val useCaseResult = useCase(useCaseParams) + Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $useCaseResult") + + if (useCaseResult.isSuccess && postSuccess) { + if (postSuccessWithData) { + sharedFlow.emit(UIResult.Success(useCaseResult.getDataOrNull())) + } else { + sharedFlow.emit(UIResult.Success()) + } + } else if (useCaseResult.isError) { + sharedFlow.emit(UIResult.Error(error = useCaseResult.getThrowableOrNull())) + } + } + } + fun ViewModel.runUseCaseWithResultAndUseCachedData( coroutineDispatcher: CoroutineDispatcher, requiresConnection: Boolean = true, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt index c32ec1d407e..1908fafd8a8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt @@ -353,7 +353,7 @@ class FileDetailsFragment : FileFragment() { thumbnailImageView.bringToFront() thumbnailImageView.isVisible = false - val file = ocFileWithSyncInfo.file + val file = ocFileWithSyncInfo.file if (ocFileWithSyncInfo.isSynchronizing) { thumbnailImageView.setImageResource(R.drawable.sync_pin) thumbnailImageView.visibility = View.VISIBLE diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt index 77c2d7f2f5e..6a1ca70b456 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt @@ -156,6 +156,7 @@ class MainFileListFragment : Fragment(), private var menu: Menu? = null private var checkedFiles: List = emptyList() + private var filesToRemove: List = emptyList() private var fileSingleFile: OCFile? = null private var fileOptionsBottomSheetSingleFileLayout: LinearLayout? = null private var succeededTransfers: List? = null @@ -474,8 +475,8 @@ class MainFileListFragment : Fragment(), } FileMenuOption.REMOVE -> { - val dialogRemove = RemoveFilesDialogFragment.newInstance(file) - dialogRemove.show(requireActivity().supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + filesToRemove = listOf(file) + fileOperationsViewModel.showRemoveDialog(filesToRemove) } FileMenuOption.OPEN_WITH -> { @@ -621,6 +622,22 @@ class MainFileListFragment : Fragment(), } } + collectLatestLifecycleFlow(fileOperationsViewModel.checkIfFileLocalSharedFlow) { + val fileActivity = (requireActivity() as FileActivity) + when (it) { + is UIResult.Loading -> fileActivity.showLoadingDialog(R.string.common_loading) + is UIResult.Success -> { + fileActivity.dismissLoadingDialog() + it.data?.let { result -> onShowRemoveDialog(filesToRemove, result) } + } + + is UIResult.Error -> { + fileActivity.dismissLoadingDialog() + showMessageInSnackbar(resources.getString(R.string.common_error_unknown)) + } + } + } + /* TransfersViewModel observables */ observeTransfers() @@ -938,6 +955,13 @@ class MainFileListFragment : Fragment(), } } + private fun onShowRemoveDialog(filesToRemove: List, isLocal: Boolean) { + val dialog = RemoveFilesDialogFragment.newInstance(ArrayList(filesToRemove), isLocal) + dialog.show(requireActivity().supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + fileListAdapter.clearSelection() + updateActionModeAfterTogglingSelected() + } + override fun onFolderNameSet(newFolderName: String, parentFolder: OCFile) { fileOperationsViewModel.performOperation(FileOperation.CreateFolder(newFolderName, parentFolder)) fileOperationsViewModel.createFolder.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult -> @@ -1099,10 +1123,8 @@ class MainFileListFragment : Fragment(), } R.id.action_remove_file -> { - val dialog = RemoveFilesDialogFragment.newInstance(checkedFiles) - dialog.show(requireActivity().supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) - fileListAdapter.clearSelection() - updateActionModeAfterTogglingSelected() + filesToRemove = checkedFiles + fileOperationsViewModel.showRemoveDialog(filesToRemove) return true } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt index 3fd471a7f9f..2993fd90820 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt @@ -36,6 +36,7 @@ import com.owncloud.android.domain.exceptions.NoNetworkConnectionException import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.CopyFileUseCase import com.owncloud.android.domain.files.usecases.CreateFolderAsyncUseCase +import com.owncloud.android.domain.files.usecases.IsAnyFileAvailableLocallyUseCase import com.owncloud.android.domain.files.usecases.ManageDeepLinkUseCase import com.owncloud.android.domain.files.usecases.MoveFileUseCase import com.owncloud.android.domain.files.usecases.RemoveFileUseCase @@ -49,7 +50,9 @@ import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.ui.dialog.FileAlreadyExistsDialog import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import timber.log.Timber @@ -68,6 +71,7 @@ class FileOperationsViewModel( private val unsetFilesAsAvailableOfflineUseCase: UnsetFilesAsAvailableOfflineUseCase, private val manageDeepLinkUseCase: ManageDeepLinkUseCase, private val setLastUsageFileUseCase: SetLastUsageFileUseCase, + private val isAnyFileAvailableLocallyUseCase: IsAnyFileAvailableLocallyUseCase, private val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, ) : ViewModel() { @@ -102,6 +106,9 @@ class FileOperationsViewModel( private val _deepLinkFlow = MutableStateFlow>?>(null) val deepLinkFlow: StateFlow>?> = _deepLinkFlow + private val _checkIfFileLocalSharedFlow = MutableSharedFlow>() + val checkIfFileLocalSharedFlow: SharedFlow> = _checkIfFileLocalSharedFlow + val openDialogs = mutableListOf() // Used to save the last operation folder @@ -123,6 +130,17 @@ class FileOperationsViewModel( } } + fun showRemoveDialog(filesToRemove: List) { + runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + showLoading = true, + sharedFlow = _checkIfFileLocalSharedFlow, + useCase = isAnyFileAvailableLocallyUseCase, + useCaseParams = IsAnyFileAvailableLocallyUseCase.Params(filesToRemove), + requiresConnection = false + ) + } + fun setLastUsageFile(file: OCFile) { viewModelScope.launch(coroutinesDispatcherProvider.io) { setLastUsageFileUseCase( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/removefile/RemoveFilesDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/removefile/RemoveFilesDialogFragment.kt index dc197ec8cce..178480d5f59 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/removefile/RemoveFilesDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/removefile/RemoveFilesDialogFragment.kt @@ -77,22 +77,19 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial * @return Dialog ready to show. */ @JvmStatic - fun newInstance(files: ArrayList): RemoveFilesDialogFragment { + fun newInstance(files: ArrayList, isAvailableLocally: Boolean): RemoveFilesDialogFragment { val messageStringId: Int var containsFolder = false - var containsDown = false var containsAvailableOffline = false for (file in files) { if (file.isFolder) { containsFolder = true } - if (file.isAvailableLocally) { - containsDown = true - } if (file.isAvailableOffline) { containsAvailableOffline = true } } + messageStringId = if (files.size == 1) { // choose message for a single file val file = files.first() @@ -109,7 +106,7 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial R.string.confirmation_remove_files_alert } } - val localRemoveButton = if (!containsAvailableOffline && (containsFolder || containsDown)) { + val localRemoveButton = if (!containsAvailableOffline && isAvailableLocally) { R.string.confirmation_remove_local } else { -1 @@ -140,7 +137,7 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial @JvmStatic @JvmName("newInstanceForSingleFile") fun newInstance(file: OCFile): RemoveFilesDialogFragment { - return newInstance(arrayListOf(file)) + return newInstance(arrayListOf(file), file.isAvailableLocally) } } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index 5de165101b5..0b9a8461542 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -75,6 +75,11 @@ class ReleaseNotesViewModel( subtitle = R.string.release_notes_4_3_0_subtitle_6, type = ReleaseNoteType.ENHANCEMENT, ), + ReleaseNote( + title = R.string.release_notes_4_3_0_title_7, + subtitle = R.string.release_notes_4_3_0_subtitle_7, + type = ReleaseNoteType.ENHANCEMENT + ) ) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt index e4a9a2a2bba..376f7787700 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt @@ -263,35 +263,43 @@ class PreviewAudioFragment : FileFragment() { mContainerActivity.fileOperationsHelper.showShareFile(file) true } + R.id.action_open_file_with -> { openFile() true } + R.id.action_remove_file -> { val dialog = RemoveFilesDialogFragment.newInstance(file) dialog.show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) true } + R.id.action_see_details -> { seeDetails() true } + R.id.action_send_file -> { requireActivity().sendDownloadedFilesByShareSheet(listOf(file)) true } + R.id.action_sync_file -> { mContainerActivity.fileOperationsHelper.syncFile(file) true } + R.id.action_set_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(listOf(file))) true } + R.id.action_unset_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(listOf(file))) true } + else -> super.onOptionsItemSelected(item) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index 7f8f3201479..9635d8048a7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -216,35 +216,43 @@ class PreviewImageFragment : FileFragment() { mContainerActivity.fileOperationsHelper.showShareFile(file) true } + R.id.action_open_file_with -> { openFile() true } + R.id.action_remove_file -> { val dialog = RemoveFilesDialogFragment.newInstance(file) dialog.show(requireFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION) true } + R.id.action_see_details -> { seeDetails() true } + R.id.action_send_file -> { requireActivity().sendDownloadedFilesByShareSheet(listOf(file)) true } + R.id.action_sync_file -> { mContainerActivity.fileOperationsHelper.syncFile(file) true } + R.id.action_set_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(listOf(file))) true } + R.id.action_unset_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(listOf(file))) true } + else -> super.onOptionsItemSelected(item) } } diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 8eac3ab9734..f84a5d2dc6a 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -739,6 +739,8 @@ The lens icon in the toolbar was removed when listing spaces in folder picker Add warning in http connections Added a warning dialog in the login view when it is a http connection + Correct \"Local only\" option in remove dialog + \"Local only\" option in remove dialog will only be shown if checking selected files and folders recursively, at least one file is available locally Open in web diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/IsAnyFileAvailableLocallyUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/IsAnyFileAvailableLocallyUseCase.kt new file mode 100644 index 00000000000..da560d6eb37 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/IsAnyFileAvailableLocallyUseCase.kt @@ -0,0 +1,46 @@ +/** + * ownCloud Android client application + * + * @author Parneet Singh + * Copyright (C) 2024 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.files.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.files.model.OCFile + +class IsAnyFileAvailableLocallyUseCase(private val fileRepository: FileRepository) : + BaseUseCaseWithResult() { + + override fun run(params: Params): Boolean = isAnyFileAvailableLocally(params.listOfFiles) + private fun isAnyFileAvailableLocally(filesToRemove: List): Boolean { + + if (filesToRemove.any { it.isAvailableLocally }) { + return true + } else { + filesToRemove.filter { it.isFolder }.forEach { folder -> + if (isAnyFileAvailableLocally(fileRepository.getFolderContent(folder.id!!))) { + return true + } + } + } + return false + } + + data class Params(val listOfFiles: List) + +}