diff --git a/owncloud-android-library b/owncloud-android-library index 19256a0cb0f..e34b35b2af0 160000 --- a/owncloud-android-library +++ b/owncloud-android-library @@ -1 +1 @@ -Subproject commit 19256a0cb0fd08c7440f6d2f754d6b3176c798c6 +Subproject commit e34b35b2af039d8acc08f1b159f62ef2ee1483ba diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java index 1a256e807d6..6523dd6690b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java @@ -32,7 +32,7 @@ import com.owncloud.android.db.UploadResult; import com.owncloud.android.domain.camerauploads.model.UploadBehavior; import com.owncloud.android.domain.files.model.OCFile; -import com.owncloud.android.usecases.UploadEnqueuedBy; +import com.owncloud.android.usecases.transfers.uploads.UploadEnqueuedBy; import com.owncloud.android.utils.MimetypeIconUtil; import timber.log.Timber; diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt index 43f42441986..948706baaf2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt @@ -81,7 +81,7 @@ val remoteDataSourceModule = module { factory { OCRemoteAuthenticationDataSource(get()) } factory { OCRemoteCapabilitiesDataSource(get(), get()) } - factory { OCRemoteFileDataSource(get(), get()) } + factory { OCRemoteFileDataSource(get()) } factory { RemoteOAuthDataSourceImpl(get(), get()) } factory { OCRemoteServerInfoDataSource(get(), get()) } factory { OCRemoteShareDataSource(get(), get()) } 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 bb0d4a5470f..90d334e1d0e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -66,14 +66,16 @@ import com.owncloud.android.domain.user.usecases.GetStoredQuotaUseCase import com.owncloud.android.domain.user.usecases.GetUserAvatarAsyncUseCase import com.owncloud.android.domain.user.usecases.GetUserInfoAsyncUseCase import com.owncloud.android.domain.user.usecases.RefreshUserQuotaFromServerAsyncUseCase -import com.owncloud.android.usecases.CancelUploadForFileUseCase -import com.owncloud.android.usecases.UploadFilesFromSAFUseCase -import com.owncloud.android.usecases.UploadFilesFromSystemUseCase -import com.owncloud.android.usecases.transfers.CancelDownloadForFileUseCase -import com.owncloud.android.usecases.transfers.CancelDownloadsForAccountUseCase -import com.owncloud.android.usecases.transfers.DownloadFileUseCase -import com.owncloud.android.usecases.transfers.GetLiveDataForDownloadingFileUseCase -import com.owncloud.android.usecases.transfers.GetLiveDataForFinishedDownloadsFromAccountUseCase +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase +import com.owncloud.android.usecases.transfers.uploads.CancelUploadForFileUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSAFUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadsForAccountUseCase +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase +import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForDownloadingFileUseCase +import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForFinishedDownloadsFromAccountUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFileInConflictUseCase import org.koin.dsl.module val useCaseModule = module { @@ -109,6 +111,7 @@ val useCaseModule = module { factory { GetFilesSharedByLinkUseCase(get()) } factory { GetFilesAvailableOfflineUseCase(get()) } factory { GetSearchFolderContentUseCase(get()) } + factory { SynchronizeFileUseCase(get(), get(), get(), get()) } factory { SortFilesUseCase() } // Sharing @@ -130,6 +133,7 @@ val useCaseModule = module { factory { GetLiveDataForFinishedDownloadsFromAccountUseCase(get()) } factory { UploadFilesFromSAFUseCase(get()) } factory { UploadFilesFromSystemUseCase(get()) } + factory { UploadFileInConflictUseCase(get()) } factory { CancelUploadForFileUseCase(get()) } // User diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index d7efb827eb9..b597d497aa5 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -24,17 +24,17 @@ package com.owncloud.android.dependecyinjection import com.owncloud.android.MainApp -import com.owncloud.android.presentation.ui.security.passcode.PasscodeAction import com.owncloud.android.presentation.ui.files.filelist.MainFileListViewModel -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel +import com.owncloud.android.presentation.ui.security.passcode.PasscodeAction import com.owncloud.android.presentation.viewmodels.authentication.OCAuthenticationViewModel import com.owncloud.android.presentation.viewmodels.capabilities.OCCapabilityViewModel import com.owncloud.android.presentation.viewmodels.drawer.DrawerViewModel import com.owncloud.android.presentation.viewmodels.files.FileDetailsViewModel -import com.owncloud.android.presentation.viewmodels.files.FilesViewModel import com.owncloud.android.presentation.viewmodels.logging.LogListViewModel import com.owncloud.android.presentation.viewmodels.migration.MigrationViewModel import com.owncloud.android.presentation.viewmodels.oauth.OAuthViewModel +import com.owncloud.android.presentation.viewmodels.releasenotes.ReleaseNotesViewModel import com.owncloud.android.presentation.viewmodels.security.BiometricViewModel import com.owncloud.android.presentation.viewmodels.security.PassCodeViewModel import com.owncloud.android.presentation.viewmodels.security.PatternViewModel @@ -46,7 +46,6 @@ import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityVie import com.owncloud.android.presentation.viewmodels.settings.SettingsVideoUploadsViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsViewModel import com.owncloud.android.presentation.viewmodels.sharing.OCShareViewModel -import com.owncloud.android.presentation.viewmodels.releasenotes.ReleaseNotesViewModel import com.owncloud.android.ui.dialog.RemoveAccountDialogViewModel import com.owncloud.android.ui.preview.PreviewImageViewModel import org.koin.androidx.viewmodel.dsl.viewModel @@ -83,10 +82,9 @@ val viewModelModule = module { viewModel { PatternViewModel(get()) } viewModel { BiometricViewModel(get(), get()) } viewModel { ReleaseNotesViewModel(get(), get()) } - viewModel { FilesViewModel(get(), get(), get()) } viewModel { PreviewImageViewModel(get(), get(), get()) } viewModel { FileDetailsViewModel(get(), get(), get(), get(), get()) } - viewModel { FileOperationViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { FileOperationsViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt index 240774814b6..7e548978fcb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt @@ -24,6 +24,7 @@ import com.owncloud.android.R import com.owncloud.android.domain.exceptions.AccountNotNewException import com.owncloud.android.domain.exceptions.AccountNotTheSameException import com.owncloud.android.domain.exceptions.BadOcVersionException +import com.owncloud.android.domain.exceptions.ConflictException import com.owncloud.android.domain.exceptions.CopyIntoDescendantException import com.owncloud.android.domain.exceptions.FileAlreadyExistsException import com.owncloud.android.domain.exceptions.FileNotFoundException @@ -63,6 +64,7 @@ fun Throwable.parseError( is ServerConnectionTimeoutException -> resources.getString(R.string.network_error_connect_timeout_exception) is ServerNotReachableException -> resources.getString(R.string.network_host_not_available) is ServiceUnavailableException -> resources.getString(R.string.service_unavailable) + is ConflictException -> resources.getString(R.string.conflict_title) is SSLRecoverablePeerUnverifiedException -> resources.getString(R.string.ssl_certificate_not_trusted) is BadOcVersionException -> resources.getString(R.string.auth_bad_oc_version_title) is IncorrectAddressException -> resources.getString(R.string.auth_incorrect_address_title) diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index ad4f1194088..b57606aed1f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -35,8 +35,8 @@ import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation; import com.owncloud.android.lib.resources.files.RemoteFile; import com.owncloud.android.operations.common.SyncOperation; -import com.owncloud.android.usecases.UploadFileInConflictUseCase; -import com.owncloud.android.usecases.transfers.DownloadFileUseCase; +import com.owncloud.android.usecases.transfers.uploads.UploadFileInConflictUseCase; +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase; import com.owncloud.android.utils.FileStorageUtils; import kotlin.Lazy; import org.jetbrains.annotations.NotNull; diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index dc9d025e6fd..80a7bb77654 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/owncloudApp/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -32,6 +32,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCUpload; import com.owncloud.android.datamodel.UploadsStorageManager; +import com.owncloud.android.domain.UseCaseResult; import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.OperationCancelledException; @@ -40,11 +41,14 @@ import com.owncloud.android.lib.resources.files.RemoteFile; import com.owncloud.android.lib.resources.files.services.implementation.OCFileService; import com.owncloud.android.operations.common.SyncOperation; +import com.owncloud.android.presentation.ui.files.filelist.MainFileListViewModel; import com.owncloud.android.presentation.ui.files.operations.FileOperation; -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel; -import com.owncloud.android.presentation.viewmodels.files.FilesViewModel; +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel; import com.owncloud.android.services.OperationsService; +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase; import com.owncloud.android.utils.FileStorageUtils; +import kotlin.Lazy; +import org.jetbrains.annotations.NotNull; import timber.log.Timber; import java.io.File; @@ -56,6 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.koin.java.KoinJavaComponent.get; +import static org.koin.java.KoinJavaComponent.inject; /** * Operation performing the synchronization of the list of files contained @@ -108,7 +113,7 @@ public class SynchronizeFolderOperation extends SyncOperation mFilesToSyncContents; + private List> mFilesToSyncContents; private List mFoldersToSyncContents; @@ -200,8 +205,8 @@ public List> getFoldersToVisit() { protected RemoteOperationResult> run(OwnCloudClient client) { final RemoteOperationResult> fetchFolderResult; - FilesViewModel filesViewModel = get(FilesViewModel.class); - filesViewModel.refreshFolder(mRemotePath); + MainFileListViewModel mainFileListViewModel = get(MainFileListViewModel.class); + mainFileListViewModel.refreshFolder(mRemotePath); mFailsInFileSyncsFound = 0; mConflictsFound = 0; @@ -277,11 +282,11 @@ private boolean folderChanged(RemoteFile remoteFolder) { } private void removeLocalFolder() { - FileOperationViewModel fileOperationViewModel = get(FileOperationViewModel.class); + FileOperationsViewModel fileOperationsViewModel = get(FileOperationsViewModel.class); ArrayList list = new ArrayList<>(); list.add(mLocalFolder); FileOperation.RemoveOperation removeOperation = new FileOperation.RemoveOperation(list, false); - fileOperationViewModel.performOperation(removeOperation); + fileOperationsViewModel.performOperation(removeOperation); } /** @@ -443,15 +448,7 @@ private boolean addToSyncContents(OCFile localFile, OCFile remoteFile) { if (shouldSyncContents && !isBlockedForAutomatedSync(localFile)) { /// synchronization for files - SynchronizeFileOperation operation = new SynchronizeFileOperation( - localFile, - remoteFile, - mAccount, - serverUnchanged, - mContext, - false - ); - mFilesToSyncContents.add(operation); + mFilesToSyncContents.add(new Pair<>(localFile, mAccount)); } } @@ -466,24 +463,25 @@ private boolean addToSyncContents(OCFile localFile, OCFile remoteFile) { * on. */ private void syncContents() throws OperationCancelledException { + @NotNull Lazy synchronizeFileUseCase = inject(SynchronizeFileUseCase.class); Timber.v("Starting content synchronization... "); - RemoteOperationResult contentsResult; - for (SyncOperation op : mFilesToSyncContents) { + for (Pair fileAndAccountPair : mFilesToSyncContents) { if (mCancellationRequested.get()) { throw new OperationCancelledException(); } - contentsResult = op.execute(getStorageManager(), mContext); - if (!contentsResult.isSuccess()) { - if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) { + SynchronizeFileUseCase.Params synchronizeFileUseCaseParams = new SynchronizeFileUseCase.Params(fileAndAccountPair.first, + fileAndAccountPair.second); + UseCaseResult result = synchronizeFileUseCase.getValue().execute(synchronizeFileUseCaseParams); + if (!result.isSuccess()) { + if (result.getDataOrNull() instanceof SynchronizeFileUseCase.SyncType.ConflictDetected) { mConflictsFound++; } else { mFailsInFileSyncsFound++; - if (contentsResult.getException() != null) { - Timber.e(contentsResult.getException(), "Error while synchronizing file : %s", - contentsResult.getLogMessage()); + if (result.getThrowableOrNull() != null) { + Timber.e(result.getThrowableOrNull(), "Error while synchronizing file : %s", fileAndAccountPair.first); } else { - Timber.e("Error while synchronizing file : %s", contentsResult.getLogMessage()); + Timber.e("Error while synchronizing file : %s", fileAndAccountPair.first); } } } // won't let these fails break the synchronization process diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt index 44c11ddf4e1..c9910dfdc1f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt @@ -22,7 +22,6 @@ package com.owncloud.android.presentation.ui.files.filelist import android.annotation.SuppressLint -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -55,7 +54,6 @@ import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.adapters.filelist.FileListAdapter -import com.owncloud.android.presentation.fold import com.owncloud.android.presentation.ui.common.BottomSheetFragmentItemView import com.owncloud.android.presentation.ui.files.SortBottomSheetFragment import com.owncloud.android.presentation.ui.files.SortBottomSheetFragment.Companion.newInstance @@ -65,15 +63,15 @@ import com.owncloud.android.presentation.ui.files.SortOrder import com.owncloud.android.presentation.ui.files.SortType import com.owncloud.android.presentation.ui.files.ViewType import com.owncloud.android.presentation.ui.files.createfolder.CreateFolderDialogFragment +import com.owncloud.android.presentation.ui.files.operations.FileOperation +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.ui.files.removefile.RemoveFilesDialogFragment -import com.owncloud.android.presentation.viewmodels.files.FilesViewModel import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.FolderPickerActivity import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.dialog.RenameFileDialogFragment import com.owncloud.android.ui.fragment.FileDetailFragment -import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.utils.ColumnQuantity import com.owncloud.android.utils.FileStorageUtils import org.koin.androidx.viewmodel.ext.android.viewModel @@ -88,12 +86,11 @@ class MainFileListFragment : Fragment(), SortOptionsView.SortOptionsListener { private val mainFileListViewModel by viewModel() - private val filesViewModel by viewModel() + private val fileOperationsViewModel by viewModel() private var _binding: MainFileListFragmentBinding? = null private val binding get() = _binding!! - private var containerActivity: FileFragment.ContainerActivity? = null private var files: List = emptyList() private lateinit var layoutManager: StaggeredGridLayoutManager @@ -146,33 +143,12 @@ class MainFileListFragment : Fragment(), menu.removeItem(menu.findItem(R.id.action_share_current_folder).itemId) } else { menu.findItem(R.id.action_share_current_folder)?.setOnMenuItemClickListener { - containerActivity?.fileOperationsHelper?.showShareFile(mainFileListViewModel.getFile()) + fileActions?.onShareFileClicked(mainFileListViewModel.getFile()) true } } } - /** - * {@inheritDoc} - */ - override fun onAttach(context: Context) { - super.onAttach(context) - Timber.v("onAttach") - containerActivity = try { - context as FileFragment.ContainerActivity - } catch (e: ClassCastException) { - throw ClassCastException( - context.toString() + " must implement " + - FileFragment.ContainerActivity::class.java.simpleName - ) - } - } - - override fun onDetach() { - containerActivity = null - super.onDetach() - } - private fun initViews() { setHasOptionsMenu(true) statusBarColorActionMode = ContextCompat.getColor(requireContext(), R.color.action_mode_status_bar_background) @@ -203,7 +179,7 @@ class MainFileListFragment : Fragment(), // Set Swipe to refresh and its listener binding.swipeRefreshMainFileList.setOnRefreshListener { - filesViewModel.refreshFolder(mainFileListViewModel.getFile().remotePath) + mainFileListViewModel.refreshFolder(mainFileListViewModel.getFile().remotePath) } // Set SortOptions and its listeners @@ -249,12 +225,9 @@ class MainFileListFragment : Fragment(), updateFileListData(fileListPostFilters) } - filesViewModel.refreshFolder.observe(viewLifecycleOwner, Event.EventObserver { - it.fold( - onLoading = { binding.swipeRefreshMainFileList.isRefreshing = true }, - onSuccess = { binding.swipeRefreshMainFileList.isRefreshing = false }, - onFailure = { binding.swipeRefreshMainFileList.isRefreshing = false } - ) + mainFileListViewModel.refreshFolder.observe(viewLifecycleOwner, Event.EventObserver { + binding.syncProgressBar.isIndeterminate = it.isLoading + binding.swipeRefreshMainFileList.isRefreshing = it.isLoading }) } @@ -425,8 +398,8 @@ class MainFileListFragment : Fragment(), } override fun onFolderNameSet(newFolderName: String, parentFolder: OCFile) { - filesViewModel.createFolder(parentFolder, newFolderName) - filesViewModel.createFolder.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult -> + fileOperationsViewModel.performOperation(FileOperation.CreateFolder(newFolderName, parentFolder)) + fileOperationsViewModel.createFolder.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult -> if (uiResult is UIResult.Error) { val errorMessage = uiResult.error?.parseError(resources.getString(R.string.create_dir_fail_msg), resources, false) @@ -497,11 +470,11 @@ class MainFileListFragment : Fragment(), val singleFile = checkedFiles.first() when (menuId) { R.id.action_share_file -> { - containerActivity?.fileOperationsHelper?.showShareFile(singleFile) + fileActions?.onShareFileClicked(singleFile) return true } R.id.action_open_file_with -> { - containerActivity?.fileOperationsHelper?.openFile(singleFile) + fileActions?.openFile(singleFile) return true } R.id.action_rename_file -> { @@ -514,16 +487,19 @@ class MainFileListFragment : Fragment(), R.id.action_see_details -> { fileListAdapter.clearSelection() updateActionModeAfterTogglingSelected() - containerActivity?.showDetails(singleFile) + fileActions?.showDetails(singleFile) return true } + R.id.action_sync_file -> { + syncFiles(listOf(singleFile)) + } R.id.action_send_file -> { //Obtain the file if (!singleFile.isAvailableLocally) { // Download the file Timber.d("%s : File must be downloaded", singleFile.remotePath) fileActions?.initDownloadForSending(singleFile) } else { - containerActivity?.fileOperationsHelper?.sendDownloadedFile(singleFile) + fileActions?.sendDownloadedFile(singleFile) } return true } @@ -623,7 +599,7 @@ class MainFileListFragment : Fragment(), if (ocFile.isFolder) { mainFileListViewModel.updateFolderToDisplay(ocFile) - filesViewModel.refreshFolder(ocFile.remotePath) + mainFileListViewModel.refreshFolder(ocFile.remotePath) } else { // Click on a file fileActions?.onFileClicked(ocFile) } @@ -680,7 +656,7 @@ class MainFileListFragment : Fragment(), val fileMenuFilter = FileMenuFilter( checkedFiles, AccountUtils.getCurrentOwnCloudAccount(requireContext()), - containerActivity, + requireActivity() as FileActivity, activity ) @@ -718,17 +694,13 @@ class MainFileListFragment : Fragment(), } } - fun syncFiles(files: Collection) { + private fun syncFiles(files: List) { for (file in files) { - file?.let { syncFile(file) } - } - } - - fun syncFile(file: OCFile) { - if (!file.isFolder) { - // TODO Sync file - } else { - filesViewModel.refreshFolder(file.remotePath) + if (file.isFolder) { + mainFileListViewModel.refreshFolder(file.remotePath) + } else { + fileActions?.syncFile(file) + } } } @@ -745,7 +717,12 @@ class MainFileListFragment : Fragment(), interface FileActions { fun onCurrentFolderUpdated(newCurrentFolder: OCFile) fun onFileClicked(file: OCFile) + fun onShareFileClicked(file: OCFile) fun initDownloadForSending(file: OCFile) + fun showDetails(file: OCFile) + fun syncFile(file: OCFile) + fun openFile(file: OCFile) + fun sendDownloadedFile(file: OCFile) fun cancelFileTransference(file: ArrayList) fun setBottomBarVisibility(isVisible: Boolean) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListViewModel.kt index 4d7a8fd3fad..0e9a55551d2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListViewModel.kt @@ -23,6 +23,7 @@ package com.owncloud.android.presentation.ui.files.filelist import android.accounts.Account import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel @@ -40,10 +41,13 @@ import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase import com.owncloud.android.domain.files.usecases.GetFolderContentAsLiveDataUseCase import com.owncloud.android.domain.files.usecases.GetSearchFolderContentUseCase +import com.owncloud.android.domain.files.usecases.RefreshFolderFromServerAsyncUseCase import com.owncloud.android.domain.files.usecases.SortFilesUseCase import com.owncloud.android.domain.files.usecases.SortType import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.extensions.isDownloadPending +import com.owncloud.android.presentation.UIResult import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.utils.FileStorageUtils @@ -59,6 +63,7 @@ class MainFileListViewModel( private val sortFilesUseCase: SortFilesUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val sharedPreferencesProvider: SharedPreferencesProvider, + private val refreshFolderFromServerAsyncUseCase: RefreshFolderFromServerAsyncUseCase, private val contextProvider: ContextProvider, private val workManager: WorkManager, ) : ViewModel() { @@ -91,6 +96,9 @@ class MainFileListViewModel( val folderContentLiveData: LiveData>> get() = _folderContentLiveData + private val _refreshFolder = MediatorLiveData>>() + val refreshFolder: LiveData>> = _refreshFolder + fun navigateTo(fileId: Long) { viewModelScope.launch(coroutinesDispatcherProvider.io) { val result = getFileByIdUseCase.execute(GetFileByIdUseCase.Params(fileId = fileId)) @@ -172,6 +180,7 @@ class MainFileListViewModel( } updateFolderToDisplay(parentDir!!) + refreshFolder(parentDir.remotePath) } } @@ -195,6 +204,18 @@ class MainFileListViewModel( ) } + fun refreshFolder( + remotePath: String + ) = runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + liveData = _refreshFolder, + useCase = refreshFolderFromServerAsyncUseCase, + showLoading = true, + useCaseParams = RefreshFolderFromServerAsyncUseCase.Params( + remotePath = remotePath + ) + ) + private fun composeFileListUiState( account: Account? = _fileListUiStateLiveData.value?.account, folderToDisplay: OCFile? = _fileListUiStateLiveData.value?.folderToDisplay, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt index 0122d86221f..343d76a2f17 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt @@ -19,11 +19,14 @@ */ package com.owncloud.android.presentation.ui.files.operations +import android.accounts.Account import com.owncloud.android.domain.files.model.OCFile sealed class FileOperation { data class CopyOperation(val listOfFilesToCopy: List, val targetFolder: OCFile) : FileOperation() + data class CreateFolder(val folderName: String, val parentFile: OCFile) : FileOperation() data class MoveOperation(val listOfFilesToMove: List, val targetFolder: OCFile) : FileOperation() data class RemoveOperation(val listOfFilesToRemove: List, val removeOnlyLocalCopy: Boolean) : FileOperation() data class RenameOperation(val ocFileToRename: OCFile, val newName: String) : FileOperation() + data class SynchronizeFileOperation(val fileToSync: OCFile, val account: Account) : FileOperation() } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationsViewModel.kt similarity index 77% rename from owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt rename to owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationsViewModel.kt index eb18dfae826..629dccdf39a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationsViewModel.kt @@ -28,25 +28,33 @@ import com.owncloud.android.domain.UseCaseResult 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.MoveFileUseCase import com.owncloud.android.domain.files.usecases.RemoveFileUseCase import com.owncloud.android.domain.files.usecases.RenameFileUseCase import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.UIResult import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import kotlinx.coroutines.launch import timber.log.Timber -class FileOperationViewModel( +class FileOperationsViewModel( + private val createFolderAsyncUseCase: CreateFolderAsyncUseCase, private val copyFileUseCase: CopyFileUseCase, private val moveFileUseCase: MoveFileUseCase, private val removeFileUseCase: RemoveFileUseCase, private val renameFileUseCase: RenameFileUseCase, + private val synchronizeFileUseCase: SynchronizeFileUseCase, private val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider ) : ViewModel() { + private val _createFolder = MediatorLiveData>>() + val createFolder: LiveData>> = _createFolder + private val _copyFileLiveData = MediatorLiveData>>() val copyFileLiveData: LiveData>> = _copyFileLiveData @@ -59,15 +67,29 @@ class FileOperationViewModel( private val _renameFileLiveData = MediatorLiveData>>() val renameFileLiveData: LiveData>> = _renameFileLiveData + private val _syncFileLiveData = MediatorLiveData>>() + val syncFileLiveData: LiveData>> = _syncFileLiveData + fun performOperation(fileOperation: FileOperation) { when (fileOperation) { is FileOperation.MoveOperation -> moveOperation(fileOperation) is FileOperation.RemoveOperation -> removeOperation(fileOperation) is FileOperation.RenameOperation -> renameOperation(fileOperation) is FileOperation.CopyOperation -> copyOperation(fileOperation) + is FileOperation.SynchronizeFileOperation -> syncFileOperation(fileOperation) + is FileOperation.CreateFolder -> createFolderOperation(fileOperation) } } + private fun createFolderOperation(fileOperation: FileOperation.CreateFolder) { + runOperation( + liveData = _createFolder, + useCase = createFolderAsyncUseCase, + useCaseParams = CreateFolderAsyncUseCase.Params(fileOperation.folderName, fileOperation.parentFile), + postValue = Unit + ) + } + private fun copyOperation(fileOperation: FileOperation.CopyOperation) { runOperation( liveData = _copyFileLiveData, @@ -104,6 +126,16 @@ class FileOperationViewModel( ) } + private fun syncFileOperation(fileOperation: FileOperation.SynchronizeFileOperation) { + runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + requiresConnection = true, + liveData = _syncFileLiveData, + useCase = synchronizeFileUseCase, + useCaseParams = SynchronizeFileUseCase.Params(fileOperation.fileToSync, fileOperation.account) + ) + } + private fun runOperation( liveData: MediatorLiveData>>, useCase: BaseUseCaseWithResult, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/removefile/RemoveFilesDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/removefile/RemoveFilesDialogFragment.kt index d113af26209..f67943563e3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/removefile/RemoveFilesDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/removefile/RemoveFilesDialogFragment.kt @@ -25,7 +25,7 @@ import android.os.Bundle import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.presentation.ui.files.operations.FileOperation -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -39,7 +39,7 @@ import java.util.ArrayList class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDialogFragmentListener { private lateinit var targetFiles: ArrayList - private val fileOperationViewModel: FileOperationViewModel by sharedViewModel() + private val fileOperationViewModel: FileOperationsViewModel by sharedViewModel() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FileDetailsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FileDetailsViewModel.kt index 60fce429dcf..d8f013319ad 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FileDetailsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FileDetailsViewModel.kt @@ -34,8 +34,8 @@ import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.preview.PreviewAudioFragment import com.owncloud.android.ui.preview.PreviewTextFragment import com.owncloud.android.ui.preview.PreviewVideoFragment -import com.owncloud.android.usecases.transfers.CancelDownloadForFileUseCase -import com.owncloud.android.usecases.transfers.GetLiveDataForDownloadingFileUseCase +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase +import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForDownloadingFileUseCase import kotlinx.coroutines.launch import java.util.UUID diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FilesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FilesViewModel.kt deleted file mode 100644 index 71d2117f120..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/files/FilesViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * ownCloud Android client application - * - * @author Abel García de Prada - * Copyright (C) 2020 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.presentation.viewmodels.files - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.ViewModel -import com.owncloud.android.domain.files.model.OCFile -import com.owncloud.android.domain.files.usecases.CreateFolderAsyncUseCase -import com.owncloud.android.domain.files.usecases.RefreshFolderFromServerAsyncUseCase -import com.owncloud.android.domain.utils.Event -import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult -import com.owncloud.android.presentation.UIResult -import com.owncloud.android.providers.CoroutinesDispatcherProvider - -class FilesViewModel( - private val createFolderAsyncUseCase: CreateFolderAsyncUseCase, - private val refreshFolderFromServerAsyncUseCase: RefreshFolderFromServerAsyncUseCase, - private val coroutineDispatcherProvider: CoroutinesDispatcherProvider -) : ViewModel() { - - private val _createFolder = MediatorLiveData>>() - val createFolder: LiveData>> = _createFolder - - private val _refreshFolder = MediatorLiveData>>() - val refreshFolder: LiveData>> = _refreshFolder - - fun createFolder( - parentFile: OCFile, - folderName: String - ) = runUseCaseWithResult( - coroutineDispatcher = coroutineDispatcherProvider.io, - liveData = _createFolder, - useCase = createFolderAsyncUseCase, - useCaseParams = CreateFolderAsyncUseCase.Params( - parentFile = parentFile, - folderName = folderName - ) - ) - - fun refreshFolder( - remotePath: String - ) = runUseCaseWithResult( - coroutineDispatcher = coroutineDispatcherProvider.io, - liveData = _refreshFolder, - useCase = refreshFolderFromServerAsyncUseCase, - showLoading = true, - useCaseParams = RefreshFolderFromServerAsyncUseCase.Params( - remotePath = remotePath - ) - ) -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt index 98f26b4fb6d..f417998d93b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt @@ -52,12 +52,12 @@ import com.owncloud.android.domain.files.usecases.RemoveFileUseCase import com.owncloud.android.domain.files.usecases.RenameFileUseCase import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.operations.RefreshFolderOperation -import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.presentation.ui.settings.fragments.SettingsSecurityFragment.Companion.PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER import com.owncloud.android.providers.cursors.FileCursor import com.owncloud.android.providers.cursors.RootCursor -import com.owncloud.android.usecases.UploadFilesFromSystemUseCase -import com.owncloud.android.usecases.transfers.DownloadFileUseCase +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.NotificationUtils import org.koin.android.ext.android.inject @@ -141,23 +141,20 @@ class DocumentsStorageProvider : DocumentsProvider() { uploadFilesUseCase.execute(uploadFilesUseCaseParams) } else { Thread { - SynchronizeFileOperation( - ocFile, - null, - getAccountFromFileId(ocFile.id!!), - false, - context, - false - ).apply { - val result = execute(currentStorageManager, context) - if (result.code == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { - context?.let { - NotificationUtils.notifyConflict( - ocFile, - getAccountFromFileId(ocFile.id!!), - it - ) - } + val synchronizeFileUseCase: SynchronizeFileUseCase by inject() + val result = synchronizeFileUseCase.execute( + SynchronizeFileUseCase.Params( + fileToSynchronize = ocFile, + account = getAccountFromFileId(ocFile.id!!)!! + ) + ) + if (result.getDataOrNull() is SynchronizeFileUseCase.SyncType.ConflictDetected) { + context?.let { + NotificationUtils.notifyConflict( + ocFile, + getAccountFromFileId(ocFile.id!!), + it + ) } } }.start() diff --git a/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java b/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java index 4fb3c2a6fb9..bffd0c96f48 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java @@ -38,7 +38,6 @@ import android.os.Process; import android.util.Pair; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.lib.common.OwnCloudAccount; @@ -50,7 +49,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.operations.CheckCurrentCredentialsOperation; -import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.common.SyncOperation; import timber.log.Timber; @@ -71,7 +69,6 @@ public class OperationsService extends Service { public static final String EXTRA_COOKIE = "COOKIE"; - public static final String ACTION_SYNC_FILE = "SYNC_FILE"; public static final String ACTION_SYNC_FOLDER = "SYNC_FOLDER"; public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS"; @@ -95,8 +92,6 @@ public Target(Account account, Uri serverUrl, String cookie) { private SyncFolderHandler mSyncFolderHandler; - private LocalBroadcastManager mLocalBroadcastManager; - /** * Service initialization */ @@ -115,9 +110,6 @@ public void onCreate() { thread = new HandlerThread("Syncfolder thread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mSyncFolderHandler = new SyncFolderHandler(thread.getLooper(), this); - - // create manager for local broadcasts - mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); } /** @@ -442,13 +434,6 @@ private Pair newOperation(Intent operationIntent) { String action = operationIntent.getAction(); if (action != null) { switch (action) { - case ACTION_SYNC_FILE: { - // Sync file - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - operation = new SynchronizeFileOperation(remotePath, account, getApplicationContext()); - - break; - } case ACTION_SYNC_FOLDER: { // Sync folder (all its descendant files are synced) String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java index 3c2bea224fa..244a07dfa32 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java @@ -28,9 +28,9 @@ import com.owncloud.android.ui.dialog.ConflictsResolveDialog; import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision; import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener; -import com.owncloud.android.usecases.UploadFileInConflictUseCase; -import com.owncloud.android.usecases.UploadFilesFromSystemUseCase; -import com.owncloud.android.usecases.transfers.DownloadFileUseCase; +import com.owncloud.android.usecases.transfers.uploads.UploadFileInConflictUseCase; +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase; +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase; import kotlin.Lazy; import org.jetbrains.annotations.NotNull; import timber.log.Timber; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index fdc2a22724d..53933348314 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -65,19 +65,16 @@ import com.owncloud.android.interfaces.ISecurityEnforced import com.owncloud.android.interfaces.LockType import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.lib.common.authentication.OwnCloudBearerCredentials -import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.resources.status.OwnCloudVersion import com.owncloud.android.operations.RefreshFolderOperation -import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.ui.files.filelist.MainFileListFragment import com.owncloud.android.presentation.ui.files.operations.FileOperation -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.ui.security.bayPassUnlockOnce import com.owncloud.android.syncadapter.FileSyncAdapter -import com.owncloud.android.ui.errorhandling.ErrorMessageAdapter import com.owncloud.android.ui.fragment.FileDetailFragment import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.fragment.TaskRetainerFragment @@ -88,11 +85,11 @@ import com.owncloud.android.ui.preview.PreviewImageFragment import com.owncloud.android.ui.preview.PreviewTextFragment import com.owncloud.android.ui.preview.PreviewVideoActivity import com.owncloud.android.ui.preview.PreviewVideoFragment -import com.owncloud.android.usecases.UploadFilesFromSAFUseCase -import com.owncloud.android.usecases.UploadFilesFromSystemUseCase -import com.owncloud.android.usecases.transfers.DOWNLOAD_ADDED_MESSAGE +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import com.owncloud.android.usecases.transfers.DOWNLOAD_FINISH_MESSAGE -import com.owncloud.android.usecases.transfers.DownloadFileUseCase +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSAFUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase import com.owncloud.android.utils.Extras import com.owncloud.android.utils.PreferenceUtils import kotlinx.coroutines.CoroutineScope @@ -155,7 +152,7 @@ class FileDisplayActivity : FileActivity(), private var localBroadcastManager: LocalBroadcastManager? = null - private val fileOperationViewModel: FileOperationViewModel by viewModel() + private val fileOperationsViewModel: FileOperationsViewModel by viewModel() var filesUploadHelper: FilesUploadHelper? = null internal set @@ -323,7 +320,6 @@ class FileDisplayActivity : FileActivity(), val secondFragment = chooseInitialSecondFragment(file) secondFragment?.let { setSecondFragment(it) - updateFragmentsVisibility(true) updateToolbar(file) } ?: cleanSecondFragment() @@ -396,6 +392,7 @@ class FileDisplayActivity : FileActivity(), val transaction = supportFragmentManager.beginTransaction() transaction.replace(R.id.right_fragment_container, fragment, TAG_SECOND_FRAGMENT) transaction.commit() + updateFragmentsVisibility(true) } fun showBottomNavBar(show: Boolean) { @@ -543,7 +540,8 @@ class FileDisplayActivity : FileActivity(), val uploadFilesFromSystemUseCase: UploadFilesFromSystemUseCase by inject() uploadFilesFromSystemUseCase.execute( UploadFilesFromSystemUseCase.Params( - accountName = account.name, listOfLocalPaths = filePaths.toList(), uploadFolderPath = remotePathBase!!) + accountName = account.name, listOfLocalPaths = filePaths.toList(), uploadFolderPath = remotePathBase!! + ) ) } else { @@ -585,7 +583,7 @@ class FileDisplayActivity : FileActivity(), val folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return val moveOperation = FileOperation.MoveOperation(listOfFilesToMove = files.toList(), targetFolder = folderToMoveAt) - fileOperationViewModel.performOperation(moveOperation) + fileOperationsViewModel.performOperation(moveOperation) } /** @@ -597,7 +595,7 @@ class FileDisplayActivity : FileActivity(), val folderToCopyAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return val copyOperation = FileOperation.CopyOperation(listOfFilesToCopy = files.toList(), targetFolder = folderToCopyAt) - fileOperationViewModel.performOperation(copyOperation) + fileOperationsViewModel.performOperation(copyOperation) } override fun onBackPressed() { @@ -1033,11 +1031,26 @@ class FileDisplayActivity : FileActivity(), override fun showDetails(file: OCFile) { val detailFragment = FileDetailFragment.newInstance(file, account) setSecondFragment(detailFragment) - updateFragmentsVisibility(true) updateToolbar(file) setFile(file) } + override fun syncFile(file: OCFile) { + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account)) + } + + override fun openFile(file: OCFile) { + if (file.isAvailableLocally) { + fileOperationsHelper.openFile(file) + } else { + startDownloadForOpening(file) + } + } + + override fun sendDownloadedFile(file: OCFile) { + fileOperationsHelper.sendDownloadedFile(file) + } + private fun updateToolbar(chosenFileFromParam: OCFile?) { val chosenFile = chosenFileFromParam ?: file // If no file is passed, current file decides @@ -1055,21 +1068,6 @@ class FileDisplayActivity : FileActivity(), } } - /** - * Updates the view associated to the activity after the finish of some operation over files - * in the current account. - * - * @param operation Removal operation performed. - * @param result Result of the removal. - */ - override fun onRemoteOperationFinish(operation: RemoteOperation<*>, result: RemoteOperationResult<*>) { - super.onRemoteOperationFinish(operation, result) - - when (operation) { - is SynchronizeFileOperation -> onSynchronizeFileOperationFinish(operation, result) - } - } - /** * Updates the view associated to the activity after the finish of an operation trying to * remove a file. @@ -1216,34 +1214,29 @@ class FileDisplayActivity : FileActivity(), } private fun onSynchronizeFileOperationFinish( - operation: SynchronizeFileOperation, - result: RemoteOperationResult<*> + uiResult: UIResult ) { - if (result.isSuccess) { - if (operation.transferWasRequested()) { - // this block is probably useless duy - val syncedFile = operation.localFile - refreshListOfFilesFragment(false) - val secondFragment = secondFragment - if (secondFragment != null && syncedFile == secondFragment.file) { - secondFragment.onSyncEvent(DOWNLOAD_ADDED_MESSAGE, false, null) - invalidateOptionsMenu() + when (uiResult) { + is UIResult.Success -> { + when (uiResult.data) { + SynchronizeFileUseCase.SyncType.AlreadySynchronized -> showSnackMessage(getString(R.string.sync_file_nothing_to_do_msg)) + is SynchronizeFileUseCase.SyncType.ConflictDetected -> showSnackMessage(getString(R.string.sync_conflicts_in_favourites_ticker)) + is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> showSnackMessage("Download enqueued") + SynchronizeFileUseCase.SyncType.FileNotFound -> { /** Nothing to do atm. If we are in details view, go back to file list */ } + is SynchronizeFileUseCase.SyncType.UploadEnqueued -> showSnackMessage("Upload enqueued") + null -> TODO() } - - } else if (secondFragment == null) { - showMessageInSnackbar( - R.id.list_layout, - ErrorMessageAdapter.getResultMessage(result, operation, resources) - ) } + is UIResult.Error -> showSnackMessage(getString(R.string.sync_fail_ticker)) + is UIResult.Loading -> { /** Not needed at the moment, we may need it later */ } } - - /// no matter if sync was right or not - if there was no transfer and the file is down, OPEN it - val waitedForPreview = fileWaitingToPreview?.let { it == operation.localFile && it.isAvailableLocally } ?: false - if (!operation.transferWasRequested() and waitedForPreview) { - fileOperationsHelper.openFile(fileWaitingToPreview) - fileWaitingToPreview = null - } +// TODO: +// /// no matter if sync was right or not - if there was no transfer and the file is down, OPEN it +// val waitedForPreview = fileWaitingToPreview?.let { it == operation.localFile && it.isAvailableLocally } ?: false +// if (!operation.transferWasRequested() and waitedForPreview) { +// fileOperationsHelper.openFile(fileWaitingToPreview) +// fileWaitingToPreview = null +// } } @@ -1296,8 +1289,7 @@ class FileDisplayActivity : FileActivity(), ) } - private fun requestForDownload(file: OCFile?) { - if (file == null) return + private fun requestForDownload(file: OCFile) { val downloadFileUseCase: DownloadFileUseCase by inject() val id = downloadFileUseCase.execute(DownloadFileUseCase.Params(account, file)) ?: return @@ -1312,14 +1304,22 @@ class FileDisplayActivity : FileActivity(), onWorkRunning = { progress -> Timber.d("Downloading - Progress $progress") }, onWorkSucceeded = { CoroutineScope(Dispatchers.IO).launch { - waitingToSend = storageManager.getFileByPath(file.remotePath) - sendDownloadedFile() + waitingToSend?.let { + waitingToSend = storageManager.getFileByPath(file.remotePath) + sendDownloadedFile() + } + waitingToOpen?.let { + waitingToOpen = storageManager.getFileByPath(file.remotePath) + openDownloadedFile() + } } }, onWorkFailed = { showMessageInSnackbar( message = String.format(getString(R.string.downloader_download_failed_ticker), file.fileName) ) + waitingToSend = null + waitingToOpen = null }, ) } @@ -1343,7 +1343,7 @@ class FileDisplayActivity : FileActivity(), */ fun startDownloadForSending(file: OCFile) { waitingToSend = file - requestForDownload(waitingToSend) + requestForDownload(file) val hasSecondFragment = secondFragment != null updateFragmentsVisibility(hasSecondFragment) } @@ -1355,9 +1355,9 @@ class FileDisplayActivity : FileActivity(), * * @param file [OCFile] to download and preview. */ - fun startDownloadForOpening(file: OCFile) { + private fun startDownloadForOpening(file: OCFile) { waitingToOpen = file - requestForDownload(waitingToOpen) + requestForDownload(file) val hasSecondFragment = secondFragment != null updateFragmentsVisibility(hasSecondFragment) } @@ -1390,7 +1390,6 @@ class FileDisplayActivity : FileActivity(), true ) setSecondFragment(mediaFragment) - updateFragmentsVisibility(true) updateToolbar(file) setFile(file) } @@ -1410,7 +1409,6 @@ class FileDisplayActivity : FileActivity(), true ) setSecondFragment(mediaFragment) - updateFragmentsVisibility(true) updateToolbar(file) setFile(file) } @@ -1426,7 +1424,6 @@ class FileDisplayActivity : FileActivity(), account ) setSecondFragment(textPreviewFragment) - updateFragmentsVisibility(true) updateToolbar(file) setFile(file) } @@ -1442,8 +1439,7 @@ class FileDisplayActivity : FileActivity(), val detailFragment = FileDetailFragment.newInstance(file, account) setSecondFragment(detailFragment) fileWaitingToPreview = file - fileOperationsHelper.syncFile(file) - updateFragmentsVisibility(true) + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account)) updateToolbar(file) setFile(file) } @@ -1535,18 +1531,21 @@ class FileDisplayActivity : FileActivity(), } private fun startListeningToOperations() { - fileOperationViewModel.copyFileLiveData.observe(this, Event.EventObserver { + fileOperationsViewModel.copyFileLiveData.observe(this, Event.EventObserver { onCopyFileOperationFinish(it) }) - fileOperationViewModel.moveFileLiveData.observe(this, Event.EventObserver { + fileOperationsViewModel.moveFileLiveData.observe(this, Event.EventObserver { onMoveFileOperationFinish(it) }) - fileOperationViewModel.removeFileLiveData.observe(this, Event.EventObserver { + fileOperationsViewModel.removeFileLiveData.observe(this, Event.EventObserver { onRemoveFileOperationResult(it) }) - fileOperationViewModel.renameFileLiveData.observe(this, Event.EventObserver { + fileOperationsViewModel.renameFileLiveData.observe(this, Event.EventObserver { onRenameFileOperationFinish(it) }) + fileOperationsViewModel.syncFileLiveData.observe(this, Event.EventObserver { + onSynchronizeFileOperationFinish(it) + }) } override fun onCurrentFolderUpdated(newCurrentFolder: OCFile) { @@ -1562,12 +1561,12 @@ class FileDisplayActivity : FileActivity(), } PreviewTextFragment.canBePreviewed(file) -> { startTextPreview(file) - fileOperationsHelper.syncFile(file) + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account)) } PreviewAudioFragment.canBePreviewed(file) -> { // media preview startAudioPreview(file, 0) - fileOperationsHelper.syncFile(file) + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account)) } PreviewVideoFragment.canBePreviewed(file) && !WorkManager.getInstance(this).isDownloadPending(account, file) -> { // FIXME: 13/10/2020 : New_arch: Av.Offline @@ -1584,7 +1583,7 @@ class FileDisplayActivity : FileActivity(), // If the file is already downloaded sync it, just to update it if there is a // new available file version if (file.isAvailableLocally) { - fileOperationsHelper.syncFile(file) + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account)) } } else -> { @@ -1593,6 +1592,10 @@ class FileDisplayActivity : FileActivity(), } } + override fun onShareFileClicked(file: OCFile) { + fileOperationsHelper.showShareFile(file) + } + override fun initDownloadForSending(file: OCFile) { startDownloadForSending(file) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java index 2028574fb96..dea8ae31802 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.java @@ -367,6 +367,26 @@ public void onFileClicked(@NonNull OCFile file) { // Nothing to do. Clicking on files is not allowed. } + @Override + public void onShareFileClicked(@NonNull OCFile file) { + // Nothing to do. Clicking on files is not allowed. + } + + @Override + public void syncFile(@NonNull OCFile file) { + // Nothing to do. Clicking on files is not allowed. + } + + @Override + public void openFile(@NonNull OCFile file) { + // Nothing to do. Clicking on files is not allowed. + } + + @Override + public void sendDownloadedFile(@NonNull OCFile file) { + // Nothing to do. Clicking on files is not allowed. + } + private class SyncBroadcastReceiver extends BroadcastReceiver { /** diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java index d4408b7692d..1a7c795c110 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java @@ -26,7 +26,6 @@ import android.accounts.AccountManagerFuture; import android.accounts.OperationCanceledException; import android.content.ContentResolver; -import android.content.Context; import android.content.Intent; import android.content.SyncRequest; import android.graphics.drawable.Drawable; @@ -52,8 +51,8 @@ import com.owncloud.android.ui.dialog.RemoveAccountDialogFragment; import com.owncloud.android.ui.dialog.RemoveAccountDialogViewModel; import com.owncloud.android.ui.helpers.FileOperationsHelper; -import com.owncloud.android.usecases.CancelUploadFromAccountUseCase; -import com.owncloud.android.usecases.transfers.CancelDownloadsForAccountUseCase; +import com.owncloud.android.usecases.transfers.uploads.CancelUploadFromAccountUseCase; +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadsForAccountUseCase; import com.owncloud.android.utils.PreferenceUtils; import kotlin.Lazy; import org.jetbrains.annotations.NotNull; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 53d8d6d36db..33c0deaa954 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -86,7 +86,8 @@ import com.owncloud.android.presentation.ui.files.ViewType; import com.owncloud.android.presentation.UIResult; import com.owncloud.android.presentation.ui.files.createfolder.CreateFolderDialogFragment; -import com.owncloud.android.presentation.viewmodels.files.FilesViewModel; +import com.owncloud.android.presentation.ui.files.operations.FileOperation; +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel; import com.owncloud.android.syncadapter.FileSyncAdapter; import com.owncloud.android.ui.adapter.ReceiveExternalFilesAdapter; import com.owncloud.android.ui.asynctasks.CopyAndUploadContentUrisTask; @@ -94,7 +95,6 @@ import com.owncloud.android.ui.fragment.TaskRetainerFragment; import com.owncloud.android.ui.helpers.UriUploader; import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.Extras; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.SortFilesUtils; import kotlin.Unit; @@ -758,10 +758,10 @@ public void optionLockSelected(@NonNull LockType type) { @Override public void onFolderNameSet(@NotNull String newFolderName, @NotNull OCFile parentFolder) { - FilesViewModel filesViewModel = get(FilesViewModel.class); + FileOperationsViewModel fileOperationsViewModel = get(FileOperationsViewModel.class); - filesViewModel.createFolder(parentFolder, newFolderName); - filesViewModel.getCreateFolder().observe(this, uiResultEvent -> { + fileOperationsViewModel.performOperation(new FileOperation.CreateFolder(newFolderName, parentFolder)); + fileOperationsViewModel.getCreateFolder().observe(this, uiResultEvent -> { UIResult uiResult = uiResultEvent.peekContent(); if (uiResult.isSuccess()) { updateDirectoryList(); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 29c2387b516..81a0ee8178c 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -38,7 +38,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.operations.CheckCurrentCredentialsOperation; import com.owncloud.android.ui.fragment.UploadListFragment; -import com.owncloud.android.usecases.RetryFailedUploadsForAccountUseCase; +import com.owncloud.android.usecases.transfers.uploads.RetryFailedUploadsForAccountUseCase; import com.owncloud.android.utils.MimetypeIconUtil; import timber.log.Timber; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java b/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java index 27fbd9c9ce9..54f50aadb25 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java @@ -55,9 +55,9 @@ import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.fragment.OptionsInUploadListClickListener; import com.owncloud.android.ui.fragment.UploadListFragment; -import com.owncloud.android.usecases.CancelUploadWithIdUseCase; -import com.owncloud.android.usecases.RetryUploadFromContentUriUseCase; -import com.owncloud.android.usecases.RetryUploadFromSystemUseCase; +import com.owncloud.android.usecases.transfers.uploads.CancelUploadWithIdUseCase; +import com.owncloud.android.usecases.transfers.uploads.RetryUploadFromContentUriUseCase; +import com.owncloud.android.usecases.transfers.uploads.RetryUploadFromSystemUseCase; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.MimetypeIconUtil; import com.owncloud.android.utils.PreferenceUtils; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java b/owncloudApp/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java index c8220230c8f..3faf585e5e7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java @@ -31,7 +31,7 @@ import androidx.work.WorkManager; import com.owncloud.android.R; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.usecases.UploadFilesFromSystemUseCase; +import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.UriUtils; import timber.log.Timber; @@ -41,9 +41,7 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.lang.ref.WeakReference; -import java.sql.Array; import java.util.ArrayList; -import java.util.List; /** * AsyncTask to copy a file from a uri in a temporal file diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt index 1ce4f51df51..985d269273e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -33,9 +33,8 @@ import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.extensions.avoidScreenshotsIfNeeded import com.owncloud.android.extensions.showMessageInSnackbar -import com.owncloud.android.lib.resources.files.FileUtils import com.owncloud.android.presentation.ui.files.operations.FileOperation -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel import com.owncloud.android.utils.PreferenceUtils import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -47,7 +46,7 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener { private var targetFile: OCFile? = null - private val filesViewModel: FileOperationViewModel by sharedViewModel() + private val filesViewModel: FileOperationsViewModel by sharedViewModel() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { targetFile = requireArguments().getParcelable(ARG_TARGET_FILE) diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/UploadListFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/UploadListFragment.java index d2a5f200dd8..6bbe6574bee 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/UploadListFragment.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/fragment/UploadListFragment.java @@ -31,7 +31,7 @@ import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.adapter.ExpandableUploadListAdapter; -import com.owncloud.android.usecases.RetryFailedUploadsUseCase; +import com.owncloud.android.usecases.transfers.uploads.RetryFailedUploadsUseCase; import kotlin.Unit; import timber.log.Timber; diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index f3e2f337522..b368e56235e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -34,6 +34,7 @@ import androidx.fragment.app.DialogFragment; import com.owncloud.android.R; +import com.owncloud.android.data.storage.LocalStorageProvider; import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.domain.sharing.shares.model.OCShare; import com.owncloud.android.lib.common.accounts.AccountUtils; @@ -41,8 +42,9 @@ import com.owncloud.android.services.OperationsService; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.dialog.ShareLinkToDialog; -import com.owncloud.android.usecases.CancelUploadForFileUseCase; -import com.owncloud.android.usecases.transfers.CancelDownloadForFileUseCase; +import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase; +import com.owncloud.android.usecases.transfers.uploads.CancelUploadForFileUseCase; +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase; import com.owncloud.android.utils.UriUtilsKt; import kotlin.Lazy; import org.jetbrains.annotations.NotNull; @@ -268,12 +270,10 @@ public void syncFiles(Collection files) { */ public void syncFile(OCFile file) { if (!file.isFolder()) { - Intent intent = new Intent(mFileActivity, OperationsService.class); - intent.setAction(OperationsService.ACTION_SYNC_FILE); - intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); - intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(intent); - + @NotNull Lazy synchronizeFileUseCaseLazy = inject(SynchronizeFileUseCase.class); + synchronizeFileUseCaseLazy.getValue().execute( + new SynchronizeFileUseCase.Params(file, mFileActivity.getAccount()) + ); } else { Intent intent = new Intent(mFileActivity, OperationsService.class); intent.setAction(OperationsService.ACTION_SYNC_FOLDER); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.kt index 3d1b857de00..c72cfb3134d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.kt @@ -38,7 +38,7 @@ import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.extensions.observeWorkerTillItFinishes import com.owncloud.android.ui.fragment.FileFragment -import com.owncloud.android.usecases.transfers.GetLiveDataForDownloadingFileUseCase +import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForDownloadingFileUseCase import com.owncloud.android.utils.PreferenceUtils import org.koin.android.ext.android.inject diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index 082a474fae1..807a43928b6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -41,11 +41,9 @@ import com.owncloud.android.authentication.AccountUtils import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.files.model.OCFile -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener -import com.owncloud.android.lib.common.operations.RemoteOperation -import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.operations.SynchronizeFileOperation -import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.domain.utils.Event +import com.owncloud.android.presentation.ui.files.operations.FileOperation +import com.owncloud.android.presentation.ui.files.operations.FileOperationsViewModel import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.fragment.FileFragment @@ -63,11 +61,10 @@ import java.util.Vector */ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, - OnPageChangeListener, - OnRemoteOperationListener { + OnPageChangeListener { private val previewImageViewModel: PreviewImageViewModel by viewModel() - private val fileOperationViewModel: FileOperationViewModel by viewModel() + private val fileOperationsViewModel: FileOperationsViewModel by viewModel() private lateinit var viewPager: ViewPager private lateinit var previewImagePagerAdapter: PreviewImagePagerAdapter @@ -125,8 +122,8 @@ class PreviewImageActivity : FileActivity(), } private fun startObservingFileOperations() { - fileOperationViewModel.removeFileLiveData.observe(this, { - if (it.getContentIfNotHandled()?.isSuccess == true) { + fileOperationsViewModel.removeFileLiveData.observe(this, Event.EventObserver { + if (it.isSuccess) { finish() } }) @@ -213,19 +210,6 @@ class PreviewImageActivity : FileActivity(), } } - override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { - super.onRemoteOperationFinish(operation, result) - if (operation is SynchronizeFileOperation) { - onSynchronizeFileOperationFinish(result) - } - } - - private fun onSynchronizeFileOperationFinish(result: RemoteOperationResult<*>) { - if (result.isSuccess) { - invalidateOptionsMenu() - } - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { @@ -277,7 +261,7 @@ class PreviewImageActivity : FileActivity(), val currentFile = previewImagePagerAdapter.getFileAt(position) updateActionBarTitle(currentFile.fileName) if (!previewImagePagerAdapter.pendingErrorAt(position)) { - fileOperationsHelper.syncFile(currentFile) + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(currentFile, account)) } // Call to reset image zoom to initial state diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt index 7f437cc0212..677fd11ff9b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt @@ -27,7 +27,7 @@ import androidx.work.WorkInfo import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase import com.owncloud.android.providers.CoroutinesDispatcherProvider -import com.owncloud.android.usecases.transfers.GetLiveDataForFinishedDownloadsFromAccountUseCase +import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForFinishedDownloadsFromAccountUseCase import kotlinx.coroutines.launch class PreviewImageViewModel( diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/synchronization/SynchronizeFileUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/synchronization/SynchronizeFileUseCase.kt new file mode 100644 index 00000000000..c65a52b5bd8 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/synchronization/SynchronizeFileUseCase.kt @@ -0,0 +1,128 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * + * Copyright (C) 2022 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.usecases.synchronization + +import android.accounts.Account +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.exceptions.FileNotFoundException +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.usecases.SaveFileOrFolderUseCase +import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadFileInConflictUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import timber.log.Timber +import java.util.UUID + +class SynchronizeFileUseCase( + private val downloadFileUseCase: DownloadFileUseCase, + private val uploadFileInConflictUseCase: UploadFileInConflictUseCase, + private val saveFileUseCase: SaveFileOrFolderUseCase, + private val fileRepository: FileRepository, +) : BaseUseCaseWithResult() { + + override fun run(params: Params): SyncType { + val fileToSynchronize = params.fileToSynchronize + val account = params.account + + CoroutineScope(Dispatchers.IO).run { + // 1. Perform a propfind to check if the file still exists in remote + val serverFile = try { + fileRepository.readFile(fileToSynchronize.remotePath) + } catch (exception: FileNotFoundException) { + // 1.1 File not exists anymore -> remove file locally (DB and Storage) + fileRepository.removeFile(listOf(fileToSynchronize), false) + return SyncType.FileNotFound + } + + // 2. File not downloaded -> Download it + if (!fileToSynchronize.isAvailableLocally) { + Timber.i("File ${fileToSynchronize.fileName} is not downloaded. Let's download it") + val uuid = requestForDownload(account = account, ocFile = fileToSynchronize) + return SyncType.DownloadEnqueued(uuid) + } + + // 3. Check if file has changed locally + val changedLocally = fileToSynchronize.localModificationTimestamp > fileToSynchronize.lastSyncDateForData!! + Timber.i("Local file modification timestamp :${fileToSynchronize.localModificationTimestamp} and last sync date for data :${fileToSynchronize.lastSyncDateForData}") + Timber.i("So it has changed locally: $changedLocally") + + // 4. Check if file has changed remotely + val changedRemotely = serverFile.etag != fileToSynchronize.etag + Timber.i("Local etag :${fileToSynchronize.etag} and remote etag :${serverFile.etag}") + Timber.i("So it has changed remotely: $changedRemotely") + + if (changedLocally && changedRemotely) { + // 5.1 File has changed locally and remotely. We got a conflict, save the conflict. + Timber.i("File ${fileToSynchronize.fileName} has changed locally and remotely. We got a conflict") + saveFileUseCase.execute(SaveFileOrFolderUseCase.Params(fileToSynchronize.copy(etagInConflict = serverFile.etag))) + return SyncType.ConflictDetected(serverFile.etag!!) + // FIXME Conflicts + } else if (changedRemotely) { + // 5.2 File has changed ONLY remotely -> download new version + Timber.i("File ${fileToSynchronize.fileName} has changed remotely. Let's download the new version") + val uuid = requestForDownload(account, fileToSynchronize) + return SyncType.DownloadEnqueued(uuid) + } else if (changedLocally) { + // 5.3 File has change ONLY locally -> upload new version + Timber.i("File ${fileToSynchronize.fileName} has changed locally. Let's upload the new version") + val uuid = requestForUpload(account, fileToSynchronize, fileToSynchronize.etag!!) + return SyncType.UploadEnqueued(uuid) + } else { + // 5.4 File has not change locally not remotely -> do nothing + Timber.i("File ${fileToSynchronize.fileName} is already synchronized. Nothing to do here") + return SyncType.AlreadySynchronized + } + } + } + + private fun requestForDownload(account: Account, ocFile: OCFile): UUID? { + return downloadFileUseCase.execute( + DownloadFileUseCase.Params( + account = account, + file = ocFile + ) + ) + } + + private fun requestForUpload(account: Account, ocFile: OCFile, etagInConflict: String): UUID? { + return uploadFileInConflictUseCase.execute( + UploadFileInConflictUseCase.Params( + accountName = account.name, + localPath = ocFile.storagePath!!, + uploadFolderPath = ocFile.getParentRemotePath(), + ) + ) + } + + data class Params( + val fileToSynchronize: OCFile, + val account: Account + ) + + sealed interface SyncType { + object FileNotFound : SyncType + data class ConflictDetected(val etagInConflict: String) : SyncType + data class DownloadEnqueued(val workerId: UUID?) : SyncType + data class UploadEnqueued(val workerId: UUID?) : SyncType + object AlreadySynchronized : SyncType + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadForFileUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadForFileUseCase.kt similarity index 92% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadForFileUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadForFileUseCase.kt index 1d6810e64c6..2a37bd43333 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadForFileUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadForFileUseCase.kt @@ -16,12 +16,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases.transfers +package com.owncloud.android.usecases.transfers.downloads import androidx.work.WorkManager import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.extensions.getWorkInfoByTags +import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD /** * Cancel every pending download for file. Note that cancellation is a best-effort diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadsForAccountUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadsForAccountUseCase.kt similarity index 92% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadsForAccountUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadsForAccountUseCase.kt index 0ed8167a065..0e99b48f240 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/CancelDownloadsForAccountUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/CancelDownloadsForAccountUseCase.kt @@ -16,12 +16,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases.transfers +package com.owncloud.android.usecases.transfers.downloads import android.accounts.Account import androidx.work.WorkManager import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.extensions.getWorkInfoByTags +import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD /** * Cancel every pending download for an account. Note that cancellation is a best-effort diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/DownloadFileUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/DownloadFileUseCase.kt similarity index 95% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/DownloadFileUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/DownloadFileUseCase.kt index 9fbd6dee508..0daaa5014b9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/DownloadFileUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/DownloadFileUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases.transfers +package com.owncloud.android.usecases.transfers.downloads import android.accounts.Account import androidx.work.OneTimeWorkRequestBuilder @@ -27,6 +27,8 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.extensions.PENDING_WORK_STATUS import com.owncloud.android.extensions.buildWorkQuery import com.owncloud.android.extensions.getTagsForDownload +import com.owncloud.android.usecases.transfers.MAXIMUM_NUMBER_OF_RETRIES +import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD import com.owncloud.android.workers.DownloadFileWorker import timber.log.Timber import java.util.UUID diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForDownloadingFileUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForDownloadingFileUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForDownloadingFileUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForDownloadingFileUseCase.kt index ebc2743b3f7..c2121b6c375 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForDownloadingFileUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForDownloadingFileUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases.transfers +package com.owncloud.android.usecases.transfers.downloads import android.accounts.Account import androidx.lifecycle.LiveData diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt similarity index 93% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt index c751dc1b8ef..1f9bf80a946 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/downloads/GetLiveDataForFinishedDownloadsFromAccountUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases.transfers +package com.owncloud.android.usecases.transfers.downloads import android.accounts.Account import androidx.lifecycle.LiveData @@ -26,6 +26,7 @@ import androidx.work.WorkManager import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.extensions.FINISHED_WORK_STATUS import com.owncloud.android.extensions.buildWorkQuery +import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD /** * Get a LiveData with the lasts downloads from an account diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadForFileUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadForFileUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadForFileUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadForFileUseCase.kt index 735a9e29285..0cb8781815a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadForFileUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadForFileUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkManager import com.owncloud.android.MainApp diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadFromAccountUseCase.kt similarity index 96% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadFromAccountUseCase.kt index 503e24ffc44..ceb465cca29 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadFromAccountUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkManager import com.owncloud.android.MainApp diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadWithIdUseCase.kt similarity index 96% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadWithIdUseCase.kt index 1740befac41..1b2f1a3a530 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadWithIdUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkManager import com.owncloud.android.MainApp diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsForAccountUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsForAccountUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsForAccountUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsForAccountUseCase.kt index cc86189940d..6e42ec6f392 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsForAccountUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsForAccountUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.content.Context import android.net.Uri diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsUseCase.kt index 723d5603c5c..b0e2480dc3b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryFailedUploadsUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryFailedUploadsUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.content.Context import android.net.Uri diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromContentUriUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromContentUriUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromContentUriUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromContentUriUseCase.kt index 667b9bf1ce5..a4bdebf4468 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromContentUriUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromContentUriUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.content.Context import androidx.core.net.toUri diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromSystemUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromSystemUseCase.kt similarity index 97% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromSystemUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromSystemUseCase.kt index 746e4ee0162..c8007de92c4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/RetryUploadFromSystemUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/RetryUploadFromSystemUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.content.Context import androidx.work.WorkManager diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadEnqueuedBy.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadEnqueuedBy.kt similarity index 96% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/UploadEnqueuedBy.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadEnqueuedBy.kt index 52e23124828..8a26adf6c0b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadEnqueuedBy.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadEnqueuedBy.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import com.owncloud.android.workers.UploadFileFromContentUriWorker diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt similarity index 98% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt index b44e58649b7..bedafea4d4b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.net.Uri import androidx.work.Constraints diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileInConflictUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileInConflictUseCase.kt similarity index 86% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileInConflictUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileInConflictUseCase.kt index 542623b2b27..e5a4e516ff8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileInConflictUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFileInConflictUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import androidx.work.Constraints import androidx.work.NetworkType @@ -31,6 +31,7 @@ import com.owncloud.android.domain.camerauploads.model.UploadBehavior import com.owncloud.android.workers.UploadFileFromFileSystemWorker import timber.log.Timber import java.io.File +import java.util.UUID /** * Use case to upload an update for a file in server. @@ -41,25 +42,26 @@ import java.io.File */ class UploadFileInConflictUseCase( private val workManager: WorkManager, -) : BaseUseCase() { +) : BaseUseCase() { - override fun run(params: Params) { + override fun run(params: Params): UUID? { val localFile = File(params.localPath) if (!localFile.exists()) { Timber.w("Upload of ${params.localPath} won't be enqueued. We were not able to find it in the local storage") - return + return null } + Timber.d("Upload file in conflict params: ${params.accountName} | ${params.localPath} | ${params.uploadFolderPath} ") val uploadId = storeInUploadsDatabase( localFile = localFile, - uploadPath = params.uploadFolderPath.plus(File.separator).plus(localFile.name), + uploadPath = params.uploadFolderPath.plus(localFile.name), accountName = params.accountName, ) - enqueueSingleUpload( + return enqueueSingleUpload( localPath = localFile.absolutePath, - uploadPath = params.uploadFolderPath.plus(File.separator).plus(localFile.name), + uploadPath = params.uploadFolderPath.plus(localFile.name), lastModifiedInSeconds = localFile.lastModified().div(1_000).toString(), accountName = params.accountName, uploadIdInStorageManager = uploadId, @@ -92,14 +94,14 @@ class UploadFileInConflictUseCase( lastModifiedInSeconds: String, uploadIdInStorageManager: Long, uploadPath: String, - ) { + ): UUID { val inputData = workDataOf( UploadFileFromFileSystemWorker.KEY_PARAM_ACCOUNT_NAME to accountName, UploadFileFromFileSystemWorker.KEY_PARAM_BEHAVIOR to UploadBehavior.COPY.name, UploadFileFromFileSystemWorker.KEY_PARAM_CONTENT_URI to localPath, UploadFileFromFileSystemWorker.KEY_PARAM_LAST_MODIFIED to lastModifiedInSeconds, UploadFileFromFileSystemWorker.KEY_PARAM_UPLOAD_PATH to uploadPath, - UploadFileFromFileSystemWorker.KEY_PARAM_UPLOAD_ID to uploadIdInStorageManager + UploadFileFromFileSystemWorker.KEY_PARAM_UPLOAD_ID to uploadIdInStorageManager, ) val constraints = Constraints.Builder() @@ -110,10 +112,13 @@ class UploadFileInConflictUseCase( .setInputData(inputData) .setConstraints(constraints) .addTag(accountName) + .addTag(uploadIdInStorageManager.toString()) .build() workManager.enqueue(uploadFileFromContentUriWorker) Timber.i("Plain upload of $localPath has been enqueued.") + + return uploadFileFromContentUriWorker.id } data class Params( diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSAFUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSAFUseCase.kt similarity index 98% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSAFUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSAFUseCase.kt index 7d23b9f101c..c8b2f8aa9ef 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSAFUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSAFUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import android.net.Uri import androidx.documentfile.provider.DocumentFile diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSystemUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSystemUseCase.kt similarity index 98% rename from owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSystemUseCase.kt rename to owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSystemUseCase.kt index 36e1f590f8c..25803589acc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFilesFromSystemUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/UploadFilesFromSystemUseCase.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.usecases +package com.owncloud.android.usecases.transfers.uploads import androidx.work.Constraints import androidx.work.NetworkType diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt index 56e34700132..db8fc4dcc17 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt @@ -36,8 +36,8 @@ import com.owncloud.android.domain.camerauploads.usecases.GetCameraUploadsConfig import com.owncloud.android.domain.camerauploads.usecases.SavePictureUploadsConfigurationUseCase import com.owncloud.android.domain.camerauploads.usecases.SaveVideoUploadsConfigurationUseCase import com.owncloud.android.presentation.ui.settings.SettingsActivity -import com.owncloud.android.usecases.UploadEnqueuedBy -import com.owncloud.android.usecases.UploadFileFromContentUriUseCase +import com.owncloud.android.usecases.transfers.uploads.UploadEnqueuedBy +import com.owncloud.android.usecases.transfers.uploads.UploadFileFromContentUriUseCase import com.owncloud.android.utils.MimetypeIconUtil import com.owncloud.android.utils.NotificationUtils import com.owncloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt index 435c9fa5891..bb35b74e825 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -43,6 +43,7 @@ import com.owncloud.android.domain.exceptions.SpecificUnsupportedMediaTypeExcept import com.owncloud.android.domain.exceptions.UnauthorizedException import com.owncloud.android.domain.files.model.OCFile.Companion.PATH_SEPARATOR import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase +import com.owncloud.android.domain.files.usecases.SaveFileOrFolderUseCase import com.owncloud.android.extensions.parseError import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudClient @@ -84,6 +85,9 @@ class UploadFileFromFileSystemWorker( private lateinit var ocUpload: OCUpload private var fileSize: Long = 0 + private lateinit var uploadFileOperation: UploadFileFromFileSystemOperation + private val saveFileOrFolderUseCase: SaveFileOrFolderUseCase by inject() + // Etag in conflict required to overwrite files in server. Otherwise, the upload will be rejected. private var eTagInConflict: String = "" @@ -99,6 +103,7 @@ class UploadFileFromFileSystemWorker( checkNameCollisionOrGetAnAvailableOneInCase() uploadDocument() updateUploadsDatabaseWithResult(null) + updateFilesDatabaseWithNewEtag() Result.success() } catch (throwable: Throwable) { Timber.e(throwable) @@ -108,6 +113,32 @@ class UploadFileFromFileSystemWorker( } } + /** + * Update the database with latest details about this file. + * + * We will ask for thumbnails after the upload + * We will update info about the file (modification timestamp and etag) + */ + private fun updateFilesDatabaseWithNewEtag() { + val currentTime = System.currentTimeMillis() + if (ocUpload.isForceOverwrite) { + val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject() + val file = getFileByRemotePathUseCase.execute(GetFileByRemotePathUseCase.Params(account.name, ocUpload.remotePath)) + file.getDataOrNull()?.let { + it.copy( + needsToUpdateThumbnail = true, + etag = uploadFileOperation.etag, + length = (File(ocUpload.localPath).length()), + lastSyncDateForData = currentTime, + modifiedAtLastSyncForData = currentTime, + etagInConflict = null + ).let { + saveFileOrFolderUseCase.execute(SaveFileOrFolderUseCase.Params(it)) + } + } + } + } + private fun areParametersValid(): Boolean { val paramAccountName = workerParameters.inputData.getString(KEY_PARAM_ACCOUNT_NAME) val paramUploadPath = workerParameters.inputData.getString(KEY_PARAM_UPLOAD_PATH) @@ -167,14 +198,14 @@ class UploadFileFromFileSystemWorker( private fun checkNameCollisionOrGetAnAvailableOneInCase() { if (ocUpload.isForceOverwrite) { - Timber.d("Upload will override current server file") val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject() val useCaseResult = getFileByRemotePathUseCase.execute(GetFileByRemotePathUseCase.Params(ocUpload.accountName, ocUpload.remotePath)) eTagInConflict = useCaseResult.getDataOrNull()?.etagInConflict.orEmpty() - Timber.d("File with the following etag in conflict: $eTagInConflict") + Timber.d("Upload will overwrite current server file with the following etag in conflict: $eTagInConflict") + return } @@ -202,7 +233,7 @@ class UploadFileFromFileSystemWorker( } private fun uploadPlainFile(client: OwnCloudClient) { - val uploadFileOperation = UploadFileFromFileSystemOperation( + uploadFileOperation = UploadFileFromFileSystemOperation( localPath = fileSystemPath, remotePath = uploadPath, mimeType = mimetype, @@ -227,7 +258,7 @@ class UploadFileFromFileSystemWorker( executeRemoteOperation { createChunksRemoteFolderOperation.execute(client) } // Step 2: Upload file by chunks - val chunkedUploadOperation = ChunkedUploadFromFileSystemOperation( + uploadFileOperation = ChunkedUploadFromFileSystemOperation( transferId = uploadIdInStorageManager.toString(), localPath = fileSystemPath, remotePath = uploadPath, @@ -238,7 +269,7 @@ class UploadFileFromFileSystemWorker( it.addDataTransferProgressListener(this) } - val result = executeRemoteOperation { chunkedUploadOperation.execute(client) } + val result = executeRemoteOperation { uploadFileOperation.execute(client) } // Step 3: Move remote file to the final remote destination val ocChunkService = OCChunkService(client) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt index 9633b10e457..91bab5b7115 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt @@ -27,6 +27,7 @@ interface LocalFileDataSource { fun copyFile(sourceFile: OCFile, targetFile: OCFile, finalRemotePath: String, remoteId: String) fun getFileById(fileId: Long): OCFile? fun getFileByRemotePath(remotePath: String, owner: String): OCFile? + fun getFileByRemoteId(remoteId: String): OCFile? fun getFolderContent(folderId: Long): List fun getSearchFolderContent(folderId: Long, search: String): List fun getSearchAvailableOfflineFolderContent(folderId: Long, search: String): List diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt index 864601a8afd..fe2fe1b9435 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt @@ -39,6 +39,8 @@ interface RemoteFileDataSource { fun moveFile(sourceRemotePath: String, targetRemotePath: String) + fun readFile(remotePath: String): OCFile + fun refreshFolder( remotePath: String ): List diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt index fbd22d42d36..c1f0b09d1bf 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt @@ -64,6 +64,9 @@ class OCLocalFileDataSource( return null } + override fun getFileByRemoteId(remoteId: String): OCFile? = + fileDao.getFileByRemoteId(remoteId)?.toModel() + override fun getFolderContent(folderId: Long): List = fileDao.getFolderContent(folderId = folderId).map { it.toModel() @@ -113,15 +116,11 @@ class OCLocalFileDataSource( ) override fun saveFilesInFolder(listOfFiles: List, folder: OCFile) { - // Insert first folder container // TODO: If it is root, add 0 as parent Id - val folderId = fileDao.mergeRemoteAndLocalFile(folder.toEntity()) - - // Then, insert files inside - listOfFiles.forEach { - // Add parent id to each file - fileDao.mergeRemoteAndLocalFile(it.toEntity().apply { parentId = folderId }) - } + fileDao.insertFilesInFolder( + folder = folder.toEntity(), + folderContent = listOfFiles.map { it.toEntity() } + ) } override fun saveFile(file: OCFile) { diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt index d80661aea88..369fea72e2f 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt @@ -22,12 +22,11 @@ package com.owncloud.android.data.files.datasources.implementation import com.owncloud.android.data.ClientManager import com.owncloud.android.data.executeRemoteOperation import com.owncloud.android.data.files.datasources.RemoteFileDataSource -import com.owncloud.android.data.files.datasources.mapper.RemoteFileMapper import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.lib.resources.files.RemoteFile class OCRemoteFileDataSource( private val clientManager: ClientManager, - private val remoteFileMapper: RemoteFileMapper ) : RemoteFileDataSource { override fun checkPathExistence( @@ -109,6 +108,14 @@ class OCRemoteFileDataSource( ) } + override fun readFile( + remotePath: String + ): OCFile = executeRemoteOperation { + clientManager.getFileService().readFile( + remotePath = remotePath + ) + }.toModel() + override fun refreshFolder( remotePath: String ): List = @@ -118,7 +125,7 @@ class OCRemoteFileDataSource( remotePath = remotePath ) }.let { listOfRemote -> - listOfRemote.map { remoteFile -> remoteFileMapper.toModel(remoteFile)!! } + listOfRemote.map { remoteFile -> remoteFile.toModel() } } override fun removeFile( @@ -142,4 +149,22 @@ class OCRemoteFileDataSource( isFolder = isFolder ) } + + private fun RemoteFile.toModel(): OCFile = + OCFile( + owner = owner, + remoteId = remoteId, + remotePath = remotePath, + length = if (isFolder) { + size + } else { + length + }, + creationTimestamp = creationTimestamp, + modificationTimestamp = modifiedTimestamp, + mimeType = mimeType, + etag = etag, + permissions = permissions, + privateLink = privateLink + ) } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/mapper/RemoteFileMapper.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/mapper/RemoteFileMapper.kt index 990fd48c336..9a999259f6a 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/mapper/RemoteFileMapper.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/mapper/RemoteFileMapper.kt @@ -22,6 +22,7 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.mappers.RemoteMapper import com.owncloud.android.lib.resources.files.RemoteFile +@Deprecated("Used by legacy code. Remove as soon as the synchronize operations are done") class RemoteFileMapper : RemoteMapper { override fun toModel(remote: RemoteFile?): OCFile? = remote?.let { diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt index cc2fac454e0..7345b6e5c30 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt @@ -41,6 +41,11 @@ abstract class FileDao { remotePath: String ): OCFileEntity? + @Query(SELECT_FILE_WITH_REMOTE_ID) + abstract fun getFileByRemoteId( + remoteId: String + ): OCFileEntity? + @Query(SELECT_FILTERED_FOLDER_CONTENT) abstract fun getSearchFolderContent( folderId: Long, @@ -88,6 +93,21 @@ abstract class FileDao { @Insert(onConflict = OnConflictStrategy.REPLACE) abstract fun insert(ocFileEntity: OCFileEntity): Long + /** + * Make sure that the ids are set properly. We don't take care of conflicts and that stuff here. + */ + @Transaction + open fun insertFilesInFolder( + folder: OCFileEntity, + folderContent: List, + ) { + val folderId = insert(folder) + + folderContent.forEach { fileToInsert -> + insert(fileToInsert.apply { parentId = folderId }) + } + } + @Transaction open fun mergeRemoteAndLocalFile( ocFileEntity: OCFileEntity @@ -248,6 +268,11 @@ abstract class FileDao { "FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME} " + "WHERE id = :id" + private const val SELECT_FILE_WITH_REMOTE_ID = + "SELECT * " + + "FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME} " + + "WHERE remoteId = :remoteId" + private const val SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH = "SELECT * " + "FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME} " + diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt index 6fe9860b852..4622fcb53f8 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt @@ -81,16 +81,16 @@ class OCFileRepository( ) } catch (targetNodeDoesNotExist: ConflictException) { // Target node does not exist anymore. Remove target folder from database and local storage and return - removeFolderRecursively(ocFile = targetFolder, removeOnlyLocalCopy = false) + removeLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false) return@copyFile } catch (sourceFileDoesNotExist: FileNotFoundException) { // Source file does not exist anymore. Remove file from database and local storage and continue if (ocFile.isFolder) { - removeFolderRecursively(ocFile = ocFile, removeOnlyLocalCopy = false) + removeLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) } else { - removeFile( + removeLocalFile( ocFile = ocFile, - onlyLocalCopy = false + onlyFromLocalStorage = false ) } return@forEach @@ -152,16 +152,16 @@ class OCFileRepository( ) } catch (targetNodeDoesNotExist: ConflictException) { // Target node does not exist anymore. Remove target folder from database and local storage and return - removeFolderRecursively(ocFile = targetFile, removeOnlyLocalCopy = false) + removeLocalFolderRecursively(ocFile = targetFile, onlyFromLocalStorage = false) return@moveFile } catch (sourceFileDoesNotExist: FileNotFoundException) { // Source file does not exist anymore. Remove file from database and local storage and continue if (ocFile.isFolder) { - removeFolderRecursively(ocFile = ocFile, removeOnlyLocalCopy = false) + removeLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) } else { - removeFile( + removeLocalFile( ocFile = ocFile, - onlyLocalCopy = false + onlyFromLocalStorage = false ) } return@forEach @@ -180,12 +180,81 @@ class OCFileRepository( } } + override fun readFile(remotePath: String): OCFile { + return remoteFileDataSource.readFile(remotePath) + } + override fun refreshFolder(remotePath: String) { - remoteFileDataSource.refreshFolder(remotePath).also { + val currentSyncTime = System.currentTimeMillis() + + // Retrieve remote folder data + val fetchFolderResult = remoteFileDataSource.refreshFolder(remotePath) + val remoteFolder = fetchFolderResult.first() + val remoteFolderContent = fetchFolderResult.drop(1) + + // Check if the folder already exists in database. + val localFolderByRemotePath: OCFile? = + localFileDataSource.getFileByRemotePath(remotePath = remoteFolder.remotePath, owner = remoteFolder.owner) + + // If folder doesn't exists in database, insert everything. Easy path + if (localFolderByRemotePath == null) { localFileDataSource.saveFilesInFolder( - folder = it.first(), - listOfFiles = it.drop(1) + folder = remoteFolder, + listOfFiles = remoteFolderContent.map { it.apply { needsToUpdateThumbnail = !it.isFolder } } ) + } else { + // Keep the current local properties or we will miss relevant things. + remoteFolder.copyLocalPropertiesFrom(localFolderByRemotePath) + + // Folder already exists in database, we need to update data + val localFolderContent = localFileDataSource.getFolderContent(folderId = localFolderByRemotePath.id!!) + + val localFilesMap = localFolderContent.associateBy { localFile -> localFile.remoteId ?: localFile.remotePath }.toMutableMap() + + // Final content for this folder, we will update the folder content all together + val folderContentUpdated = mutableListOf() + + // Loop to sync every child + remoteFolderContent.forEach { remoteChild -> + remoteChild.lastSyncDateForProperties = currentSyncTime + + // Let's try with remote path if the file does not have remote id yet + val localChildToSync = localFilesMap.remove(remoteChild.remoteId) ?: localFilesMap.remove(remoteChild.remotePath) + + // If local child does not exists, just insert the new one. + if (localChildToSync == null) { + folderContentUpdated.add( + remoteChild.apply { + parentId = localFolderByRemotePath.id + needsToUpdateThumbnail = !remoteChild.isFolder + // remote eTag will not be set unless file CONTENTS are synchronized + etag = "" + }) + } else { + // File exists in the database, we need to check several stuff. + folderContentUpdated.add( + remoteChild.apply { + copyLocalPropertiesFrom(localChildToSync) + // DO NOT update etag till contents are synced. + etag = localChildToSync.etag + needsToUpdateThumbnail = + !remoteChild.isFolder && remoteChild.modificationTimestamp != localChildToSync.modificationTimestamp + // FIXME: What about renames? Need to fix storage path + }) + } + } + localFileDataSource.saveFilesInFolder( + folder = remoteFolder, + listOfFiles = folderContentUpdated + ) + // Remaining items should be removed from the database and local storage. They do not exists in remote anymore. + localFilesMap.map { it.value }.forEach { ocFile -> + if (ocFile.isFolder) { + removeLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) + } else { + removeLocalFile(ocFile = ocFile, onlyFromLocalStorage = false) + } + } } } @@ -199,9 +268,9 @@ class OCFileRepository( } } if (ocFile.isFolder) { - removeFolderRecursively(ocFile, removeOnlyLocalCopy) + removeLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy) } else { - removeFile(ocFile, removeOnlyLocalCopy) + removeLocalFile(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy) } } } @@ -245,26 +314,26 @@ class OCFileRepository( localFileDataSource.saveFile(file) } - private fun removeFolderRecursively(ocFile: OCFile, removeOnlyLocalCopy: Boolean) { + private fun removeLocalFolderRecursively(ocFile: OCFile, onlyFromLocalStorage: Boolean) { val folderContent = localFileDataSource.getFolderContent(ocFile.id!!) // 1. Remove folder content recursively folderContent.forEach { file -> if (file.isFolder) { - removeFolderRecursively(file, removeOnlyLocalCopy) + removeLocalFolderRecursively(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage) } else { - removeFile(file, removeOnlyLocalCopy) + removeLocalFile(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage) } } // 2. Remove the folder itself - removeFile(ocFile, removeOnlyLocalCopy) + removeLocalFile(ocFile = ocFile, onlyFromLocalStorage = onlyFromLocalStorage) } - private fun removeFile(ocFile: OCFile, onlyLocalCopy: Boolean) { + private fun removeLocalFile(ocFile: OCFile, onlyFromLocalStorage: Boolean) { localStorageProvider.deleteLocalFile(ocFile) - if (onlyLocalCopy) { - localFileDataSource.saveFile(ocFile.copy(storagePath = null)) + if (onlyFromLocalStorage) { + localFileDataSource.saveFile(ocFile.copy(storagePath = null, etagInConflict = null)) } else { localFileDataSource.removeFile(ocFile.id!!) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt index cf1af2b78fe..92637a8e09b 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt @@ -36,6 +36,7 @@ interface FileRepository { fun getFilesSharedByLink(owner: String): List fun getFilesAvailableOffline(owner: String): List fun moveFile(listOfFilesToMove: List, targetFile: OCFile) + fun readFile(remotePath: String): OCFile fun refreshFolder(remotePath: String) fun removeFile(listOfFilesToRemove: List, removeOnlyLocalCopy: Boolean) fun renameFile(ocFile: OCFile, newName: String)